htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

式途中に null として評価されてしまうケースの回避方法

F# の話題です。
let x = A().B.C.D とあるとき、もしA().Bnull だったら?
F# で定義した型は AllowNullLiteralAttribute 属性を付与するか Unchecked.defaultof<'a> を使用しない限り、たとえ参照型だろうが null になることはありません。
でもCLRのクラスは null の可能性があります。
.NET Framework の資産を使う以上、 NullReferenceException と付き合っていく必要はどうしてもあります。

一番愚直な方法は、いわゆる nullチェックです。

let x =
    match A() with
    | null -> null
    | x ->
        match x.B with
        | null -> null
        | x ->
            match x.C with
            | null -> null
            | x -> x.D

…ツラすぎる。

次、Maybeモナドみたいなコンピュテーション式を使う方法。

[<AutoOpen>]
module Maybe =
    type MaybeBuilder() =
        member __.Bind (x, f) = if x = null then null else f x
        member __.Return x = x
    let maybe = MaybeBuilder()

let x = maybe {
    let! x = A()
    let! x = x.B
    let! x = x.C
    return x.D }

さっきよりは大分マシになりましたね。

まぁこれでいいんですが、もう一歩やってみました。

gist.github.com

クッソ長いんですが、要は評価したい式を引用式として与えることで、途中で null を検出したら全体を null とするってことです。
nullNone、非 nullSome x として見れば、まさにMaybeモナドがやっていることとニアリーイコールの結果を得られるわけです。

Gist のコメントに書いたやつのコピペですが、

open System
open QuotationUtility

let nullUri : Uri = null
let result1 = Quotation.evaluate <@ "not null value" @>
let result2 = Quotation.evaluate <@ (Uri("http://example.com/")).AbsoluteUri @>
let result3 = Quotation.evaluate <@ (null : Uri).AbsoluteUri @>
let result4 = Quotation.evaluate <@ nullUri.AbsoluteUri @>

printfn "1 : [%A] / 2 : [%A] / 3 : [%A] / 4 : [%A]" result1 result2 result3 result4
(* 1 : ["not null value"] / 2 : ["http://example.com/"] / 3 : [<null>] / 4 : [<null>] *)

大体こんな感じになります。
なんとなーく動いてるように見えますね?
上の例で言えば let result = Quotation.evaluate <@ A().B.C.D @> とすることで、仮に A().Bnull だったとしても result には null が束縛されるだけです。もちろん D まで正しく評価できれば D の値が束縛されます。

たぶんバグあるので複雑な式を与えるとエラると思いますが、少なくとも単純な式なら null を検出できているようです。
これを使えばスマートに nullチェックできるんじゃないでしょうか。
リフレクションなので、評価回数が増えてくると速度低下に繋がるところがネックですね。

ところで、F#のシンタックスハイライト、なんか毒々しいですね。