Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第7章 null 安全

「値がないこと」を表す null は、多くの言語で実行時エラーの王様です。null チェックを1箇所忘れただけで、プログラムはある日突然落ちる — 発明者自身が「10億ドルの過ち」と悔やんだ仕掛けです。

Lune は null を廃止する代わりに、型で飼いならす道を選びました。null が入りうる場所を型 T? で宣言させ、確かめる前に使うことをコンパイラが許さない。第5章の Option と似た役割ですが、こちらは専用の構文(??、?.、絞り込み)を持つ軽量版です。使い分けは章の最後に整理します。

7.1 T? — null を持てる型

型に ? を付けると、その型の値または null を持てるようになります。

lune> let present: Int? = 42
ok
lune> let absent: Int? = null
ok
lune> present
42 : Nullable[Int]

表示に出てくる Nullable[Int] は Int? の内部名です(読み替えてください)。Int の値はそのまま Int? の場所に入りますが、逆は許されません。

lune> def wantsInt(n: Int): Int =
...     n + 1
...
ok
lune> wantsInt(present)
error[TYP0003]: Int が必要ですが、Nullable[Int] が見つかりました
   = help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください

これが null 安全のすべてです。「null かもしれない値」は、null でないことを確かめるまで、普通の値として使えない。確かめる方法がこの章の残りです。

7.2 match で外す — null の被覆と絞り込み

一番基本の方法は match です。orzero.lune:

module orzero

# T? には T の値も null も入る。
let present: Int? = 42

let absent: Int? = null

# null を match で被覆すると、残りの腕の v は Int に絞り込まれる。
def orZero(value: Int?): Int =
    match value:
        | null -> 0
        | v -> v
$ lune --eval unwrapped orzero.lune
42
$ lune --eval defaulted orzero.lune
0

| null -> の腕が null を引き受けたあとなので、次の腕の v は Int に絞り込まれます(narrowing)。だから v をそのまま Int として返せるのです。

網羅性検査(第5章)は null もケースとして数えます。missingnull.lune:

module bad

# Bool? の match は true / false だけでは足りない。
def toInt(b: Bool?): Int =
    match b:
        | true -> 1
        | false -> 0
$ lune --check missingnull.lune
error[TYP0007]: 網羅的でない match: null のケースがありません
  --> missingnull.lune:5:5
  |
5 |     match b:
  |     ^^^^^ パターン null がカバーされていない
   = hint: null のケースを追加するか、ワイルドカードケース `| _ -> ...` を追加してください
   = help: 詳しくは `lune explain TYP0007 --lang ja` を実行してください

「null チェックを忘れる」というあの事故が、コンパイル時の null のケースがありません に変わりました。

一つ注意。名前パターンだけで受けると、網羅性は満たせますが絞り込まれません。

lune> def bad(value: Int?): Int =
...     match value:
...         | v -> v
...
error[TYP0003]: 分岐: Int が必要ですが、Nullable[Int] が見つかりました
  --> <repl:17>:3:16
  |
3 |         | v -> v
  |                ^ この式の型は Nullable[Int]
   = help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください

v が Int になるのは、null の腕がそれより前にあるときだけです。

7.3 ?? — なければこれ

「null ならデフォルト値」は頻出なので、専用の演算子があります。null 合体演算子 ?? です。

lune> absent ?? 7
7 : Int
lune> present ?? 7
42 : Int
lune> present ?? crash()
42 : Int

左が null でなければ左を、null なら右を返します。結果の型は Int? ではなく Int — 合体した時点で null の可能性は消えるからです。3つ目の例に注目してください。左に値があるとき、右側は評価すらされません。第4章の遅延評価が、ここでも当たり前のように効いています。

7.4 ?. — 安全にたどる

「user が null でなければ user.name を読みたい」— これも専用構文があります。安全ナビゲーション ?. です。nameof.lune:

module nameof

record User:
    name: String
    age: Int

# ?. は受け手が null なら null に短絡し、そうでなければフィールドを読む。
def nameOf(user: User?): String? =
    user?.name

let someName = nameOf(User(name = "Ada", age = 36))

let noName = nameOf(null)

let fallback = nameOf(null) ?? "(nobody)"
$ lune --eval someName nameof.lune
"Ada"
$ lune --eval noName nameof.lune
null
$ lune --eval fallback nameof.lune
"(nobody)"

受け手が null なら結果も null、そうでなければフィールドの値。結果は常に String? になるので、最後は ?? や match で受け止めます。?. で null を運び、?? で着地させる — この2つはセットで使う道具です。

7.5 if で絞り込む

match を書くまでもない場面では、if の条件でも絞り込めます。

lune> def orOne(x: Int?): Int =
...     if x != null then x else 1
...
ok
lune> orOne(absent)
1 : Int

x != null が真の分岐では、x は Int として使えます(x == null なら偽の分岐で絞り込まれます)。この絞り込みが効くのは x != null / x == null という単純な形だけで、&& で繋いだ複合条件や elif には及びません。凝った条件になったら match に切り替えてください。

7.6 null を返す関数を書く

ここまでは null を受け取る側でした。返す側は、素直に書けば通ります。

lune> def maybeDiv(x: Int, y: Int): Double? =
...     if y == 0 then null else x / y
...
ok
lune> maybeDiv(7, 2)
3.5 : Nullable[Double]
lune> maybeDiv(7, 0)
null : Nullable[Double]

null を返す腕と Double を返す腕が、なぜ揉めずに合流できるのか。第5章 §5.6 の先取りボックスで予告した仕組みがここで働いています — 戻り値注釈 Double? が期待型として if の両分岐へ配られ、null の腕も x / y の腕もそこで Double? に揃うのです。match の腕でも同じことが起きます。ファイル版の maybediv.lune:

module maybediv

# 期待型 Double? が if の両分岐へ配られ、null の腕と x / y の腕がそこで合流する。
def maybeDiv(x: Int, y: Int): Double? =
    if y == 0 then null else x / y

let some = maybeDiv(7, 2)

let none = maybeDiv(7, 0)

let fallback = maybeDiv(7, 0) ?? 0.0
$ lune --eval some maybediv.lune
3.5
$ lune --eval none maybediv.lune
null
$ lune --eval fallback maybediv.lune
0.0

maybeDiv(7, 0) で 0 除算エラーが出ないのは、if が選ばなかった腕を評価しないからです(第4章)。「失敗しうる計算」を else の腕に置いたまま、null で受け流す — T? を返す関数の基本形です。

こう書いても動く — 分岐の前に、注釈付き let で部品を作っておく書き方もあります。

# 部品を注釈付き let で先に作ってから分岐しても動く。
# quotient はサンクなので、y == 0 の側では割り算は一度も走らない。
def maybeDivLet(x: Int, y: Int): Double? =
    let quotient: Double? = x / y
    if y == 0 then null else quotient

maybeDivLet(7, 0) もちゃんと null を返します。let quotient: Double? = x / y を「通って」いるのに 0 除算エラーが出ないのは、quotient がサンクのまま一度も force されないから — こちらも第4章の遅延評価です。どちらを選んでも安全なので、普段は分岐を先に書く素直な形で十分です。

「なくてもよい」フィールドの読み出し(?.)、デフォルトへの着地(??)、そしてこの「なければ null を返す関数」。3点セットで、null は型に守られた普通の道具になります。

7.7 Option[T] と T? の使い分け

似た道具が2つある理由と、選び方です。

T?Option[T]
「なし」の表現nullNone
専用構文??、?.、if/match の絞り込みなし(match と関数で扱う)
相性フィールド・引数・戻り値の「なくてもよい」プレリュードのリスト API(head/tail が返す)、ジェネリック関数
入れ子の「なし」表せない(null は1段)Some(None) と None を区別できる

実用の目安はこうです。データの形として「ないかもしれない」を書くなら T?(レコードのフィールド、関数の引数と戻り値)。リスト処理や高階関数のパイプラインの中では Option(プレリュードがそちらを話すからです)。境界では match で詰め替えれば行き来できます。

壊してみよう — orZero の2つの腕を入れ替えると(| v -> v を先に)どうなるでしょう。予想してから試してください。

lune> def swapped(value: Int?): Int =
...     match value:
...         | v -> v
...         | null -> 0
...
error[TYP0003]: 分岐: Int が必要ですが、Nullable[Int] が見つかりました
  --> <repl:19>:3:16
  |
3 |         | v -> v
  |                ^ この式の型は Nullable[Int]
   = help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください

到達不能(TYP0009)を予想した人が多いはずですが、その手前で捕まりました。先頭の v はまだ null が被覆されていないので Int? のままです(7.2節の注意)。そこへ戻り値注釈の Int が期待型として配られ(7.6節)、最初の腕がその期待に応えられないと名指しされます。絞り込みは「null の腕が先」のときだけ働く — 腕の順番は型にまで影響します。

まとめ

概念一言で
T?T または null。表示名は Nullable[T]
型の守りT? を T として使うと TYP0003。null ケース漏れは TYP0007
match| null -> を被覆すると、以降の名前パターンは T に絞り込み
??null ならデフォルト。右辺は必要になるまで評価されない
?.null なら null に短絡してフィールドを読む。?? とセットで
if x != null単純形のみ絞り込み。複雑になったら match
null を返す期待型が分岐へ配られるので、素直に if/match で書ける(第5章 §5.6 と同じ仕組み)
vs Optionデータの形は T?、リスト・高階関数の世界は Option

演習問題

演習 7-1(★) 結果(値と型)を予想してから確かめてください。absent: Int? = null、present: Int? = 42 とします。

absent ?? 7
present ?? 7
absent == null
present ?? crash()
解答

7 : Int、42 : Int、true : Bool、42 : Int。最後の式が爆発しないのは ?? が短絡するからです。型が Int? ではなく Int になっていることも確認してください。

演習 7-2(★★) 7.5節の orOne を、if ではなく match で書き直してください。

解答
lune> def orOne(x: Int?): Int =
...     match x:
...         | null -> 1
...         | v -> v
...
ok
lune> orOne(null)
1 : Int
lune> orOne(41)
41 : Int

orZero と同じ骨組みです。if x != null 版と match 版はどちらも正解で、分岐が2択なら if、増えるなら match、が目安です。

演習 7-3(★★) nameOf にならって ageOf を書き、ageOf(null) ?? 0 が 0 になることを確かめてください。

解答
lune> def ageOf(user: User?): Int? =
...     user?.age
...
ok
lune> ageOf(User(name = "Ada", age = 36))
36 : Nullable[Int]
lune> ageOf(null) ?? 0
0 : Int

「?. で運んで ?? で着地」の型 User? → Int? → Int の流れを、:type でも追ってみてください。

演習 7-4(★★★) 整数リストの平均を返す average(xs: List[Int]): Double? を書いてください。空リストの平均は null とします。0 除算を起こさずに書けるでしょうか。

解答
module answers

# 演習 7-4: 空リストの平均は「ない」。先に分岐すれば 0 除算には触れない。
def average(xs: List[Int]): Double? =
    if isEmpty(xs) then null else fold(xs, 0, fn a x -> a + x) / length(xs)

let some = average([2, 3, 4])

let none = average([])

let safe = average([]) ?? 0.0
$ lune --eval some ex7-4.lune
3.0
$ lune --eval none ex7-4.lune
null
$ lune --eval safe ex7-4.lune
0.0

空かどうかを先に分岐してしまえば、割り算は空でないリストに対してしか評価されません。null の腕と計算の腕が Double? へ合流するのは 7.6節のとおりです。合計と割り算を注釈付き let で先に定義してから分岐する書き方でも、サンクのまま捨てられるので 0 除算にはなりません(7.6節の「こう書いても動く」)。

演習 7-5(★・逆転問題) 「null かもしれない値を、確かめずに使った」ことを型検査に叱られる最小のコードを書いてください。

解答
let present: Int? = 42

let oops = present + 1

とすると error[TYP0003]: +: 数値型が必要ですが、Nullable[Int] が見つかりました が出ます。関数に渡す形(本文 7.1 節の wantsInt(present))なら Int が必要ですが、Nullable[Int] が見つかりました。他言語なら実行時の NullPointerException になっていた事故が、コンパイル時のこの1行に置き換わっている — それが null 安全の価値です。


より正確には — T? の型付け・narrowing の正確な規則は documents/LANGUAGE_SPEC.md §11(match の null パターン)と §9.2(if の絞り込み)、null を含む網羅性は documents/MATCH_EXHAUSTIVENESS_SPEC.md、期待型が分岐へ配られる規則は documents/LOCAL_TYPE_INFERENCE_SPEC.md §5.3。実例は samples/nullable.lune にもあります。この章のコード例は books/examples/ch07/ にあり、すべて実際の CLI で検証されています。