先日社内で事業部のエンジニア全体向けに発表する機会があり、テーマが割と何でもありだったのでNull安全について話しました。発表の中で話せなかったことなども踏まえてブログにもまとめてみようと思います。
まずこのテーマを選んだ理由ですが、背景として
- 自分がちょうどSwiftでiOSアプリを開発しており、Null安全な言語を利用していた
- 事業部でNull安全な言語を使っているプロジェクトが他になかった
- 比較的近年になって広がりを見せている新しい概念である
- 自分がSwiftを書き始めて一番感動した部分だった
- 将来的に言語選定の際の判断軸になるなど、具体的なアクションにつながりそう
といったところがあり、共有する価値があるのではと考えました。そういうわけなのでできるだけ特定の言語に依存せず、まったく知らない人にその概念を理解してもらい、良さを伝えるということを念頭に置いています。
Null安全とは
Null安全とは、一言で言うと「Nullチェックをコンパイラが強制する仕組み」のことです。Nullチェックをしないとコンパイルが通らない状態にすることで、実行時にNullへの不正なアクセスを防ぐ、ということをゴールとしています。
概念の説明としてはこれだけなのですが、Null安全が導入された背景を理解するため、NullとNull安全を取り巻く歴史を紐解きつつ、さらに深掘りしていこうと思います。
Nullの発祥
Nullという概念は1965年にTony HoareがALGOL Wという言語に導入したのが始まりとされています。その後様々な言語でNullが導入されていますが、2009年のカンファレンス(QCon)でTony Hoare自身が「Nullは10億ドルに相当する誤りだった」という旨の発言をしています。さらに、当時からコンパイラが参照の安全性をチェックするという発想はありながらも、実装の簡易さから単にNullを導入するという選択をしてしまい、多くのクラッシュにつながってしまった、とも話しています。
気をつけてNullチェックをする、みたいなことは当たり前のようになってしまいがちですが、人間が気をつけないとバグにつながるようなことはできるだけ機械に任せたほうが良いでしょう。考案者本人が認めているように、Nullチェックも本来はもっと仕組みで自動化されるべき存在だということではないでしょうか。コンパイラによって自動化するというのはその1つのアプローチといえます。
Null安全の歴史
Null安全という言葉は2005年のカンファレンス(ECOOP)でBertrand Meyerによって「Void Safety」という名前で紹介されたのが最初のようです。その後本人が開発するプログラミング言語Eiffelに導入されています。実装レベルではそれ以前にも似たような仕組みを実験的に導入している言語はいくつかあったみたいです。
それなりにメジャーな言語に普及していったのはここ数年のことで、だいたい2010年ごろからです。2010年に登場したRustをはじめ、Kotlin(2011~), Swift(2014~), TypeScript 2.0(2016~)など様々な言語に導入されました。
コンピュータやプログラミング言語の歴史から考えれば比較的近年になって広まってきた考え方であるといえるでしょう。
Null安全の仕組み
もう少し具体的な仕組みに踏み込んで見たいと思います。Null安全は実際にどういった形で実装されているのでしょうか。いろいろなバリエーションはあるかと思いますが、個人的に一番慣れ親しんでいるSwiftを例に取りつつ説明していきます。
Null安全はその性質上、言語機能として実装されることが多いです。やや雑な説明かもしれませんが、以下の2つの条件が満たせればNull安全な言語であるといえるでしょう。
- Optional型が存在すること
- Optionalでない型にはNullを代入できないこと
Optionalという言い方は言語によって異なるかと思いますが、わりとよく使われている用語のようです。概念としては「値もしくはNullを代入できるが、その中身には直接アクセスできない」ような型だと考えていただければよいかと思います。Optional型は明示的にNullチェックをすることによって中身の値にアクセスできるようになります。この型を使っていれば、Nullへの不正なアクセスは起こり得ません。
また、Optionalでない型にNullを代入できてしまうと結局そちらでNullへの不正なアクセスが起きうるため、2つめの条件も保証されている必要があります。
コード例を見る
Swiftでのコード例を見てみましょう。Swift未経験の方でもわかるようにコメントを含めていますのでなんとなく理解してもらえればと思います。まずOptional型です。
var opt: String? // StringのOptional型を定義する opt = nil // Optionalなのでnilを代入できる opt.lowercased() // Error! NullチェックをしていないのでStringのメソッドは呼べない if let non_opt = opt { // Nullチェック non_opt.lowercased() // NullチェックをしたのでStringのメソッドを呼べる }
Swiftの場合、型名の後ろに?
を付けることでOptionalにすることができます。また、nilがいわゆるNullです。Optionalにはnilを代入でき、チェックなしには値にアクセスしたりメソッドを読んだりすることはできません。
次に、Optionalでない型です。
var non_opt: String = "TEST" non_opt = nil // Error! Optionalでないのでnilは代入できない non_opt.lowercased() // Stringのメソッドを呼べる
Optionalでない型にはnilは代入できません。つまりnilにはなりえないのでチェックなしにメソッドを呼ぶことができます。Non-null typeと呼ぶこともあります。
Javaなど従来の多くの言語では参照型のみNull代入可能であることが多いですが、Swiftでは値型か参照型か、ということとNull代入できるかどうかということは関係ありません。IntやDoubleといった値型でもOptionalならNull代入できますし、クラスなどの参照型でもOptionalでなければNullは代入できません。
これによって、型が値型か参照型かは意識せず、「存在しない」という状態をNullで表せるようになります。
Null安全のメリット
Null安全についてイメージが深まってきたでしょうか。次にNull安全のメリットについて見ていこうと思います。メリットとしては以下の様なものがあると思います。
うっかりNullチェックを忘れてしまうことがなくなる
コンパイラがNullチェックを強制するためです。Nullチェックをするか否かの判断軸も明確になるため、全体的にNullについての悩みが減ります。
生産性が高まる
開発のイテレーションにおいてNullチェック忘れというありがちなミスを早い段階で見つけられるようになるため、実装, コンパイル, 実行, デバッグ, というイテレーションを早めることができ、生産性が高まることが期待できます。
関数のインタフェースが表現豊かになる
これは直感的には理解しづらいかもしれませんが、例えば関数が失敗した場合の簡単なエラーの返し方としてOptional型を使うことができます。
エラーの表現として-1や空文字を返すようなパターンは古くから存在しますが、-1や空文字を失敗として扱うと理解しづらくなりますし、失敗しうるのかどうかがドキュメントを読まない限りわかりません。Nullを返すパターンは多少ましですが、結局失敗しうるのかどうかは関数のシグニチャからはわかりません。エラーをthrowする書き方は良いのですが、「書くコスト」は無視できません。チームで開発する場合、メンバーのスキル感によってはコストの高い書き方に対してリターンを理解してもらうための学習コストもかかります。それらを加味して、Optionalを返すというエラーの返し方は手軽でありながらエラーを返す可能性がある関数なのかどうかがわかりやすいため可読性も高いのが良いと感じています。
また、引数がOptionalであれば文字通りオプショナルな引数と判断することもできます。このように、関数のインタフェースが表現豊かになるのは大きなメリットといえます。
Null安全の悩ましい点
メリットの多いNull安全ですが、悩ましい点もあります。
抜け道の存在
現実的に、Optional型でも処理的にNullにならないケースもあります。そのような場合に冗長性を廃するため、Null安全な言語でありながらNullチェックを回避する仕組みが多くの言語に存在しています。
例えばSwiftにはForced Unwrappingと呼ばれる仕組みがあります。これは正しく使えば冗長なチェックを減らすことができるのですが、間違って使ってしまうと結局実行時にNullにアクセスしてしまい、クラッシュにつながってしまいます。Forced Unwrappingするには、変数の後ろに!
を付けます。
var opt: String? // StringのOptional型 opt = nil // Optional型なのでnil代入できる var non_op = opt! // Forced Unwrapping。中身はnilなので実行時にクラッシュする。
正しく使えれば便利なこともあるのですが、これによって結局Nullによるクラッシュを100%なくすには至っていません。これはNull安全の仕組みを採用しないほどのデメリットとは考えていませんが、もう少し良い仕組みにもできそうな気がしています。
Null安全を導入する悩ましさ
これは使う上での話というよりはそもそも導入する上での障壁の話です。
Null安全な言語であることの条件の1つとして「Optionalでない型にはNullを代入できないこと」というものを上げました。これは、例えばJavaにOptionalを導入しても、通常のNull代入可能な参照型があることによってNull安全の恩恵を受けにくくなってしまうということです。
このような背景があるため、既存のNull安全でない言語にNull安全を導入するのは難しいところがあります。Null安全なしくみを利用したければ、そのようなしくみのある言語を採用する必要があるでしょう。そのためプロジェクトの初期に決定する必要があり、すでに動いているプロジェクトに導入するのは難しいと言えます。
ちなみに、AltJSの1つであるTypeScriptはそのへんをうまくやっていて、バージョン2.0からデフォルトでは後方互換性を保ちつつ、フラグをたてることで後方互換性を捨てる代わりにNull安全の機能を導入できるようになっているみたいです。とはいえ、後方互換性をもたせることの重要性が高い言語ではそのようなアプローチも難しいのかもしれないと思っています。
Non-Null Typeについて
Null安全について説明するとOptional型に注目してしまいがちなのですが、本当に強力なのはそれと対をなすOptionalでない型、つまりNon-Nullな型の存在です。
Optionalな型が生まれたときに素早くチェックし、登場する変数をOptionalでない型のみにできれば、そこはNullが存在しない世界、ロジックに集中できる世界です。Nullにまつわる煩わしい問題をプログラムの端に追いやって、ロジックの本質的な領域を増やすことができれば見通しの良いプログラムを組むことにつながるでしょう。
おわりに
近年のプログラミング言語における新しい概念としてNull安全について解説しました。
プログラミング言語は人間に優しくなるように進化しています。構造化プログラミング、オブジェクト指向、型安全など、どれも可能なこと自体は大きく変わっていませんが、人間にとって読みやすく、書きやすく、管理しやすくするためのアイディアです。Null安全もその一つと言えるのではないでしょうか。
Null安全は、Nullがもたらす煩わしさを低減し、コアなロジックに集中できるようにする強力なツールであると感じています。正しく活用できれば多くのメリットが得られるでしょう。
Swift界隈では色々と議論され、「良い」という認識が広まっているテーマな割に、コミュニティによってはまだ全然浸透していないと感じています。Null安全という考え方に興味を持ってもらうきっかけになれば幸いです。