DartのNull安全導入状況 (NNBD)

# はじめに
NNBDは純粋にDartの機能ですが、そのDartの最近の人気は一重にFlutterの人気によるものです。また、Dart 2.0でAOTコンパイルに最適化した理由のひとつとして、Flutterの存在が大きいでしょう。JITよりもAOTに親和性の高いNNBDもまた、Flutter向けと言うことが出来ます。より多くの人に読んでもらうためにFlutterタグも付けました。

# Non-Null By Default (NNBD)とは
DartではNull安全のことを通称でNNBDと呼びます。これまでは`int i;`と書くと`i = null;`が可能でしたが、NNBD導入後はコンパイル時エラーとなります。Null可型にするには`int? i;`と書くことになります。`?`修飾のない既存の`int`の挙動を変えて非Null可型にするので、By Defaultです。Null安全を知らない方にとっては、これだけでは何のことやらわからないと思いますが、下記に良い記事が有るので説明を譲ります。

- [null安全でない言語は、もはやレガシー言語だ - Qiita](http://qiita.com/koher/items/e4835bd429b88809ab33 "null安全でない言語は、もはやレガシー言語だ - Qiita")
- [null安全を誤解している人達へのメッセージ - Qiita](http://qiita.com/omochimetaru/items/ee29d4c6eb0d78f02b15 "null安全を誤解している人達へのメッセージ - Qiita")

ただし、以降の説明のために、スマートキャストについて触れておきます。

```dart
f(int? x){
  if(x != null){
    return x * 2;
  }
}
```
上記において、`if`文の中では`x`が`int?`から`int`にキャスト(type promotionと言ったりするのでややこしい)され、コンパイルエラー無く`x * 2`を実行できます。

# NNBDの開発ステータス
3年前の時点でDartチームの中ではNNBDについて既に長い時間を議論に費やしており、私も当時の[Dart Advent Calendar 2016の記事](https://qiita.com/Cat_sushi/items/196a3d1e8fec45d9be12 "Dart型システムの動向 - Qiita")にしました。そのNNBDがついに、2019/5/13に[仕様策定中から実装中のスタータスに](https://github.com/dart-lang/language/issues/110#event-2335342391 "Null Safety: Sound non-nullable types with incremental migration (NNBD) · Issue #110 · dart-lang/language")なりました。とはいえ、細部に決まっていない仕様がたくさんあります。12/8時点では、[dart-lang/languageのnnbdタグがあるissuesのうち29/79しかcloseしていません](https://github.com/dart-lang/language/issues?q=is%3Aissue+is%3Aopen+label%3Annbd "Issues · dart-lang/language")し、その分母は日々増えています。[NNBDリリースの時期もバージョンも決まっていません](https://github.com/dart-lang/language/blob/master/accepted/future-releases/nnbd/feature-specification.md "language/feature-specification.md at master · dart-lang/language")が、かなり大きなbreaking changeになるので、バージョンは3.0になるのではないでしょうか。並行して実装は着々と進められており、既に[dart-lang/sdkにはNNBDのタグが付いたissueが232あり、そのうち79がclose](https://github.com/dart-lang/sdk/issues?q=is%3Aissue+is%3Aopen+label%3ANNBD "Issues · dart-lang/sdk")しています。

# DartのNNBDの特徴
DartのNNBDは一端のNull安全になるはずですが、それに必要な言語機能の説明の多くを先の参照記事に譲り、ここではDartで特有かつ特徴的な部分を取り上げます。

## 型階層
Topが`Object?`となり、その直下に`Null`と`Object`がぶら下がります。`Null`は唯一のインスタンス`null`を持つBottomです。`Object`側のBottomは新たに導入するインスタンスを持たない`Never`になります。

また、`int?`は`int`と`Null`のタグ無しUnion型であってFlatteningされます。つまり、

```dart
Map<String, int?> m = {'a': 1, 'b': null, 'c': 3};
print(m['d']); // => null
```

上記ではキー`'d'`が無いのか、キー`'d'`に`null`が格納されているのか区別できません。他方で、Dart SDKやアプリケーションがシンプルに書けるというメリットがあります。

## マイグレーション
先述の通りNNBDはbreaking changeですが、既存コードのマイグレーションをサポートする機能が導入されます。

### マイグレーションツール
フォーマッタdartfmtの応用で、非NNBDなDartコードをNNBDなDartコードにコンバートするツールが導入されます。オールマイティでは無いが、移行を大きく手助けするツールになるはずです。Dartチームはマイグレーションツールをとても重要視していて、[Issues · dart-lang/sdk](https://github.com/dart-lang/sdk/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aanalyzer-nnbd-migration+ "Issues · dart-lang/sdk")にはanalyzer-nnbd-migrationダグが付いているものが既に90もあります。

### 段階的マイグレーション
issue名に[Null Safety: Sound non-nullable types with incremental migration (NNBD)](https://github.com/dart-lang/language/issues/110 "Null Safety: Sound non-nullable types with incremental migration (NNBD) · Issue #110 · dart-lang/language")とあるとおり、既存のアプリやライブラリの段階的な移行を支援する機能が盛り込まれます。移行期間では、NNBDなアプリからNNBDでないライブラリを利用することも、その逆もできます。ちなみに、アプリも一種のライブラリです。

#### ライブラリ別SDKバージョン指定
NNBDなDart SDKで非NNBDなライブラリが壊れないように、想定するDart SDKバージョンをライブラリ単位に指定します。例えば、ファイルの冒頭`library`宣言の前に`// @dart = 2.6`等と書くと、NNBDを導入したDart SDK 3.0も、そのライブラリをDart 2.6時代の非NNBDなものとして解釈します。

これはDart SDKの複雑化を招くので、一定の移行期間を置いてNNBD一筋になります。

#### NNBD導入コードと非導入コードの混在 (2020/1/23 修正)
ライブラリ別SDKバージョン指定は、新SDKが後方互換(既存コードが前方互換)にならないだけのbreaking change(SDKへの言語制限追加)には十分ですが、同時に旧SDKが前方互換(新コードが後方互換)ですらない(SDKの言語の意味論を変更する)場合には不十分です。NNBDなコードとNNBDでないコード間でやり取りする情報の意味論が変わるNNBDでは、両者の境界における不整合を解決しなければなりません。ちなみに、Dart 2.0の原型であるStrong Modeは概ね新SDKが後方非互換、旧SDKが前方互換の言語制限追加でした。

NNBDの移行期間において、NNBDでないライブラリの型`int`は`int*`という特殊な内部型になります。当然ながら`int*`型の変数には`null`を格納できますが、`int*`は`int?`と同様にスマートキャストで`int`になりえます。他方で、`int*`のメンバ参照(メソッド呼び出し)は可能であり、NNBDにおける非NULL可型の側面もあります。なお、NNBDでない`int`をNNBDな`int`と区別する目的でも`int*`の表示がエラーメッセージ等で役に立ちますが、プログラマがコードに記述することはできません。

このあたりの規則は難解で良く理解できていないところも多々あります。分かったら改めて解説する予定です。

また、NNBD移行期間中は非Null可型の変数等への代入箇所全てにコンパイラが実行時nullチェック(assertion)を挿入します。この性能インパクトが大きすぎる場合は、Dart 1におけるChecked Modeのようなデバッグモードを導入するかもしれません。

逆に、非NNBDなライブラリでNNBDなライブラリの型や値を利用する場合の特段の配慮はありません。

上記の対応をしたとしても、NNBD移行期間中のNull安全を保証できるわけではなく、コンパイラの最適化もできません。この観点からも、NNBD移行期間は一定の期間で終了し、その後のDart SDKはNNBD一筋になります。

## 非Null可型変数の遅延初期化
初期化子もコンストラクタの初期化リストも無い非Null可型のフィールドや変数の宣言には修飾子`late`を付けます。これは暫定値で初期化することが許されない`final`の場合に特に有効です。例えば、`late final int a;`とします。`a`は参照時までに初期化されていればよく、初期化されていなければ実行時エラーとなります。

## 省略不可名前付きパラメタ
省略不可な名前付きパラメタには`required`修飾子を付けます。これまでも同様の目的で`@required`アノテーションが有りましたが、これと異なり`required`は言語の一級機能です。特徴的なのは、`required`であることが自明なデフォルト値無し非Null可型のパラメタでも`requred`が必須であることです。これには[活発な議論](https://github.com/dart-lang/language/issues/156 "How should default parameter values work with NNBD? · Issue #156 · dart-lang/language")があり、私も省略できる方を押したのですが、結果的に必須になりました。その判断の決め手は、デフォルト値は関数型の一部ではないというもの。`Function({int x=0})`とは書けず`Function({int x})`となるので、これと区別するためには省略不可なパラメタを`Function({required int x})`とする必要があります。すると、

```dart
typedef F = int Function({required int x})
int f1({int x}){...}
int f2({required int x}){...}
int f3({int x=0}{...}
```

上記のうち、原理的には`f1`と`f2`を同じ型`F`とみなすこともできますが、`F`と`f1`でシグニチャの見た目が異なることになります。これは、一貫性を重要視する言語仕様の世界ではNGだ、ということです。そこかよ!

## Null Awareメンバ参照の簡略記法
`a`がNull可型であった場合、メンバ参照は`a?.b`と書けます。これがネストする場合`a?.b?.c?.d`と書きます。ここで`b`が非null可型の場合に`a?.b.c?.d`と書けるのが簡略記法です。`a`が`null`だった場合、`a?.b`の評価結果は`null`であるにもかかわらず、です。

なお、`c`がNull可型であれば再び`c?.d`と`?`が必要になります。

メソッド呼び出しの場合も同様です。例えば、`a?.b().c()?.d()`のように書きます。

## `List`や`Map`のNull Aware要素参照 (2020/1/22文法変更)
`a`がNull可型の場合のNull Awareな要素参照は`a?[b]`と書きます。これがネストする場合は`a?[b]?[c]?[d]`と書きます。`a[]`が非Null可型の場合は簡略記法で`a?[b][c]?[d]`と書けます。

Dart 2.7でプレビューリリースしたNNBDでは`a?.[b]`でした。その理由は要素参照が`a?[b]`では`{a?[b]:c}`が`a?[b]`をキー、`c`をバリューとする要素を持つ`Map`リテラルなのか、3項演算`a ? [b] : c`を要素に持つ`Set`リテラルなのか、心理的に曖昧というものです。

しかし、これが不評だったため`a[b]`に変更になりました。不評だった理由は`a.b`、`a.b()`、`a..b`、`a..b()`、`a..[b]`、`...a`のNull Awareな記法が全て`a`の直後に`?`を追加するだけ、すなわち`a?.b`、`a?.b()`、`a?..b`、`a?..b()`、`a?..[b]`、`...a?`であるのに対して、`a[b]`のNull Awareな記法が`a?.[b]`であることも心理的負荷が高いというものです。

文法的にも曖昧でしたが、3項演算に解釈できるところは常に3項演算として解釈することで解決しています。いわゆるぶら下がり`else`問題を常に直近の`if`に対応付けることで解決するのに似ています。明らかな`Map`リテラル文脈`<int, int>{a?[b]:c}`であっても`a?[b]:c`を3項演算として解釈し、その結果キー・バリューのペアの`Map`要素でないとしてエラーを出力します。`Map`リテラルとして解釈させる場合は`{(a?[b]):c}`と括弧を付ける必要がありますが、`Map`のキーに`null`を期待するケースは稀であろうという判断です。

なお、文法上の曖昧性を解決する別の有力策に`?[`をトークンとするというものが有りましたが、これはスペースセンシティブな文法になる問題がありました。`?[`をトークンとして採用しなかったのは、3項演算をスペース無しで`a?[b]:c`と書く人がそれなりに居るという判断でしょう。特に採用した解決策には自動フォーマット前提のタイプインでスペース分のキーストロークを減らす効果があります。ちなみに`?.`はトークンです。

これに併せて同じ問題を抱えていたNull Awareな関数実行も`a.?()`から`a?()`に変わります。

この議論には私も参加していました。Dartチームは最近まで、[`a?b:c`の形の3項演算が無くならない限り`a?[b]`の形の要素参照は無い](https://github.com/dart-lang/language/issues/376#issuecomment-534989958 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")、と頑なでしたが、この記事を書くために過去の議論を整理し、それを[GitHubコメント](https://github.com/dart-lang/language/issues/376#issuecomment-564045517 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")に投稿したあたりから流れが変わったと自負しています。

以下記録のために残しておきますが、[おわりに](#おわりに "DartのNull安全導入状況 (NNBD) - Qiita")まで飛んでください。

>【削除】
>`[]`も演算子の一種ですが、そのNull Awareな記法に特徴があります。`a`がNull可型の場合は`a?.[b]?.[c]?.[d]`と書きます。`a?[b]?[c]?[d]`では**ありません**。

>`a[]`が非Null可型の場合は簡略記法で`a?.[b][c]?.[d]`と書けます。

>記法に`.`(ドット)が余計と感じるかと思いますが、その判断の決め手は、`{a?[b]:c}`がプログラマ心理的に曖昧である、というもの。つまり、`List`や`Map`のNull Awareな要素参照結果をキーとする`Map`リテラル`{(a?[b]): c}`なのか、3項演算子を用いた`Set`リテラル`{a ? ([b]) : c}`なのか、わかりにくいということ。文法的には`?[`を一つのトークンとすることで解決できるのですが、そういう問題ではないとのことです。

>ちなみに、`a ? b : c`の形の3項演算子、`{a, b, c}`の形の`Set`リテラルの双方を持つ言語はDartぐらいのものです。Dartも2019/2/27にバージョン2.2で`Set`リテラルを導入したばかりです。やっちまったんじゃないでしょうか。

>`?[`と`?.[`のどちらにすべきかについてDartチームメンバが挙げた[issue](https://github.com/dart-lang/language/issues/376 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")で議論されています。このissueは`?.[`に収束するかたちで[2019/6/1に一旦close](https://github.com/dart-lang/language/issues/376#event-2382107468 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")しますが、[2019/8/4のMediumへの公式ポスト](https://medium.com/dartlang/dart-nullability-syntax-decision-a-b-or-a-b-d827259e34a3 "Dart nullability syntax decision: a?[b] or a?.[b] - Dart - Medium")をきっかけに議論が再開し、[2019/8/21に再びopen](https://github.com/dart-lang/language/issues/376#event-2571033114 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")して今に至ります。今ではコメント数が93件で、dart-lang/languageの中ではclose済みのStatic Extension Methodの113件についで[歴代2位](https://github.com/dart-lang/language/issues?utf8=%E2%9C%93&q=is%3Aissue+sort%3Acomments-desc+ "Issues · dart-lang/language")、NNBDタグが付くもののなかではダントツの1位です。

>議論が白熱した理由には、`a?.[b]`において`.`(ドット)が一見冗長であるため`a?.b`や`a?.b()`他から連想できず、覚えにくいという純粋な反対意見の他に、下記のような説得力の弱い理由が`a?.[b]`支持派から多数挙げられて議論が迷走したことがあると思います。

>- `a?.[b]`支持派の主張の凡例
>  - 上記主張に対する反論の凡例
 
>-----

>- **要素参照が`a?[b]`では`{a?[b]:c}`が心理的に曖昧。** // 今のところこれが決め手、というか信念。
    - 曖昧だろうと3項演算子より要素参照の方がずっと重要。 // という反論も有る。
    - むしろ3項演算子こそdeprecateすべき。  // ← いまここ
    - 式が唯一の要素を持つ`Set`リテラルで、その要素が3項演算で、その第2項が唯一の要素を持つ`List`リテラルで、第3項が`List`であることなんて、超レアじゃない?
- 要素参照が`a?[b]`では`{a?[b]:c}`が文法的に曖昧である。
    - `?[`を一つのトークンにすれば解決する。
- 空白センシティブな構文はよろしくない。 
    - 既に`--a`と`- -a`は空白センシティブ。
    - フォーマッタとアナライザからのエラーが問題を緩和するはず。
- `?.`は他の演算子`a?.+(b)`等にも応用できる。
    - それを言うなら要素参照は`a?.[](b)`でしょ。
    - `a?.*(b).+(c)?./(d)`とか奇妙。
    - `?+`、`?-`、`?*`、`?/`がトークンでも良いはず。
- JavaScriptが`a?.[b]`である。
    - JavaScriptは文法において見習うべき言語ではない。
    - JavaScriptの`a?.[b]`は提案中であり、仕様以前である。
- `a?[`は3項演算子の書き始めを想起させ、混乱の元である。
    - 評判は知らないけど、ともかくSwiftやC#は`a?[b]`を受け入れた。
- `a?.[b]?.[c]?.[d]`の方が、メソッドチェーンであることがわかりやすい。
    - 同じくメソッドチェーンの`a[b][c][d]`が定着しており、問題にもなっていない。
- 特にNull Awareの簡略記法を利用する場合にメソッドチェーンを意識することが重要。
    - `a?[b][c]?[d]`と書けるし、問題とは思えない。
- `a?.[b]`は`a?.b`、`a?.b()`、`a?..b`、`a?..[b]`に似ている。
    - `a?.b`、`a?.b()`、`a?..b`、`a?..[b]`は`.`(ドット)を追加していないので、比較がおかしい。
    - 余計な`.`(ドット)を導入しない`a?[b]`の方がよっぽど似ている。

# おわりに
既存コードの開発者にある程度の痛みを伴うエンハンスですが、同時に開発者からの期待の大きな機能でもあります。開発にはまだしばらくかかり、2020年中にリリースできれば御の字といったところかもしれませんが、今後も注視していきたいと思います。

# 2019/12/11 追記
早くもプレビューリリースされましたね。
[Announcing Dart 2.7: A safer, more expressive Dart - Dart - Medium](https://medium.com/dartlang/dart-2-7-a3710ec54e97#a79a "Announcing Dart 2.7: A safer, more expressive Dart - Dart - Medium")

# 2019/12/29 追記
いままで、[Issue #376](https://github.com/dart-lang/language/issues/376# "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")はDartチームが質問に答えたり、`a?.[b]`反対派を説得するissueでしたが、[このコメント](https://github.com/dart-lang/language/issues/376#issuecomment-567167570 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")で流れが変わったように思います。3項演算子が無くならない限り`a?[b]`は無い、と頑なだったDartの言語チームがF2Fでミーティングを開ました。まだ何も決まっていないと言いつつ、「`a?[b]`は技術的には可能」、「関係した技術者は軒並み`a?[b]`を好む」、「`a?.[b]`の強い賛同者は(外部に)居ない」、「`a![b]`と対称的なのは良い」と`a?[b]`採用にも前向きです。

`a?[b]`に賛同する方は是非[このコメント](https://github.com/dart-lang/language/issues/376#issuecomment-567167570 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")に👍してください。

その前に`{a?[b]:c}`の心理的曖昧性を解決すべきという方は、是非[Deprecate ternary operator ?/:](https://github.com/dart-lang/language/issues/740 "Deprecate ternary operator ?/: · Issue #740 · dart-lang/language")にも👍してください。

# 2020/1/23 追記
プレビューリリース後ですら外部からの声で文法さえも変更するDartチームは非常にオープンだと感じました。私には、強い主張をしながらも口やかましいマイノリティーになってやいないかと一抹の不安もありましたが、Dartチームはコミュニティの建設的な議論に[ここ](https://github.com/dart-lang/language/issues/376#issuecomment-566766373 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")とか[ここ](https://github.com/dart-lang/language/issues/376#issuecomment-576924506 "Should null-aware subscripting use `?[` or `?.[` syntax? · Issue #376 · dart-lang/language")で敬意を示しています。Dartをより良いものにするために、皆さんも積極的に議論に参加してみてはいかがでしょうか。

# 2022/8/19 追記
`f?()`は実現していませんでした。
`f?.()`でもなく`f?.call()`と書く必要があります。