このサイトでの挿絵は、Gemini Proによる生成が大半です。 また、情報収集・まとめなどのサイトについては、検索結果をベースとしたものを使用しています。可能な限り出典を明示するよう努めますが完全ではありません。各自で調べる事もお忘れなく。

「無」は、生まれたときは安全だった——10億ドルの過ちと、?.の一行に宿る60年〈言語知新(3)〉

こんにちは!Agy無限会社のコンテンツ制作部です。

今日、あなたも user?.name ?? "Unknown" のような一行をどこかで書いたはずです。この何気ない一行には、実は60年分の歴史が詰まっています。2020年に行われた大規模なJavaアプリケーションのエラー解析では、記録されたエラーの97%がわずか10種類のエラーに起因しており、その筆頭がNull参照関連だったという報告もあります。NullPointerException(存在しない値を無理に呼び出そうとしたときに発生するエラー)で画面が真っ白になった経験は、決して他人事ではありません。今回の言語知新は、「無」という概念がどう安全に生まれ、どう危険になり、どう救済されてきたかを、地層を掘るように現代から過去へたどっていきます。



1. 現代地層——標準装備になった「無」の管理

2014年に登場したSwiftの オプショナル型 は、実はコンパイラの魔法ではありません。Optional<Wrapped> という標準ライブラリの列挙型で、型名末尾の ? はそのシンタックスシュガーに過ぎないのです。player?.highScore ?? 0 というオプショナルチェーニングのおかげで、かつての「ネストしたif文の地獄(ピラミッド・オブ・ドゥーム)」は姿を消しました。

Kotlin(2016年)はスマートキャストとエルビス演算子(?:)で実用主義を貫き、Dart 2.12(2021年)は型階層そのものを再構築して 健全なNull安全 (コンパイラが「非Nullの変数には実行時にも絶対にnullが入らない」と数学的に保証できる仕組み)を実現しました。これにより、無駄なnullチェックをバイナリから削除できるという徹底ぶりです。一方でC# 8.0(2019年)は、巨大な既存コードベースを壊さないよう、Null許容参照型をオプトインの「警告」として後付けしました。同じ目的地に向かいながら、言語ごとに事情がにじみ出ているのが面白いところです。


2. 過渡期地層——13年の空白を埋めた理論と実装

1990年、Haskellは Maybe型 という形で値の不在を扱いました。存在しないことを示す Nothing と、値がある状態を示す Just という2つのデータコンストラクタを持つ、シンプルな設計です。個人的には、今回掘り返した地層の中でも、この潔い二択の設計が一番好きです。

1992年、Philip Wadlerは論文の中で、圏論の概念である モナド (値を文脈ごと包んだまま、安全に処理を連鎖させるための仕組み)をプログラミング言語の副作用制御に応用しました。モナドは、値を文脈に包む return と、包まれた値に関数を適用する >>=(バインド演算子)という2つの基本操作を持ち、途中で Nothing が発生すれば以降の処理を自動でスキップし、最終結果を Nothing のままにする性質を内側に隠し持っています。この「文脈を保ったまま安全に計算をつなぐ」振る舞いが、20年以上後にSwiftやKotlinへ持ち込まれる「オプショナルチェーニング」の理論的な土台になりました。

Haskellの登場から13年後の2003年、Niceという言語も見逃せません。JVM上で動く言語ながら、?String のようにNull許容型を明示し、 フロー解析 (コードの実行順序を追跡し、変数の状態変化を検知する仕組み)によって「nullチェック済みなら以降は安全」と判定する仕組みをすでに持っていました。Kotlinのスマートキャストの完全な先駆けなのに、歴史の影に隠れてしまった不遇の存在です。理論と実装が出そろってなお、産業界の主流に届くまでにはさらに10年以上かかることになります。


3. 形式化地層——「書き忘れ」をコンパイラが見抜く

さらに遡ると、エディンバラ大学のRobin Milnerらが設計したStandard ML(1983年設計開始、1990年仕様定義)にたどり着きます。ここで確立された option型NONE / SOME of 'a)は、 代数的データ型 (複数の型を1つにまとめ、場合分けできるようにするデータ構造)と パターンマッチ (値の形に応じて処理を振り分ける構文)の組み合わせにより、コンパイラが「NONEのケースを書き忘れていないか」を静的に検証できる仕組みを持っていました。

これが「網羅性検証」です。もともと定理証明系の道具として生まれたMLが、「絶対に推論を間違えてはいけない」という要求から、値を取り出す前に必ず存在確認を強制する設計にたどり着いたというのは、なんだか背筋が伸びる話です。

もしこの網羅性検証がなければ、どうなるでしょうか。NONE の処理を書き忘れたコードは、コンパイル時には何のエラーも出さずに素通りしてしまいます。問題が表面化するのは、実際にその分岐を実行時に踏んだ瞬間——まさにALGOL W以降の言語が長年抱えてきたのと同じ種類の事故です。Standard MLがコンパイラにこの見落としを機械的に検出させたことは、「値の不在」の扱いを、プログラマの注意力任せから型システムの保証へと引き上げた大きな転換点でした。現代のSwiftの switch 文の網羅性検証やRustの match も、突き詰めればこのSMLの発明の延長線上にあります。


4. 原罪地層——1965年、たった一つの妥協

そして本題です。そもそも、なぜ型安全性を破壊するNull参照がプログラミング言語に混入したのか。答えは1965年のALGOL Wにあります。Niklaus WirthとTony Hoareが設計したこの言語において、当初Hoareが目指したのは「参照の利用が常に絶対的に安全で、コンパイラが自動チェックする型システム」でした。

しかし、Hoareは「実装のしやすさ」という誘惑に打ち勝てませんでした。木構造の終端などを表現するのに、参照変数へ「何も指していない状態」を持たせるのは、メモリ割り当ての観点からもコンパイラ実装の観点からも圧倒的に簡単だったのです(実装が楽だから……というその言い訳、駆け出しエンジニアの「とりあえず動けばいいや」と同じ匂いがしませんか)。2009年、ロンドンのQConカンファレンスで彼は「実装がとても簡単だったからというだけの理由で、Null参照を入れる誘惑に抗えなかった」と公の場で懺悔し、この妥協が C言語 のNullポインタや、C++、Java、C#といった巨大な産業用言語群へとそのまま受け継がれたと振り返っています。Hoare自身の見立てによれば、この決定はその時点ですでに40年にわたって無数の実行時エラーやシステムクラッシュを引き起こし、10億ドル規模の損害をもたらしていたといいます。


5. 始祖地層——NILは、もともと危険な記号ではなかった

最後にたどり着くのは1960年、John McCarthyの論文「Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I」です。Lispの NIL は、ALGOL Wのような「メモリ上の参照先が存在しない」というハードウェア都合の産物ではありません。(A . (B . (C . NIL))) のように、リストの終端を示す数学的な基底(Base case)として導入された、純粋に記号的な存在でした。

しかもNILは多義的で、真偽値の「偽」を兼ね、空の環境(変数の束縛リスト)を示す役割も担っていました。たとえば万能関数applyを評価する際、変数の束縛を持たない初期環境として空の連想リストNILが渡され、リストの評価関数や引数のクォート関数においても、再帰の終端を示すベースケースとして機能します。1つの記号が「リストの終わり」「偽」「空の環境」という複数の役割を同時に担っていたという設計は、後の言語がNullを単一の「不在」の印としてしか扱わなかったこととは対照的です。危険なポインタどころか、再帰関数の停止条件を支える、極めて安全な記号処理の構成要素だったのです。「無」は、生まれたときは何も壊さない存在だったという事実が、この地層の一番深いところに眠っています。


まとめ

「無」は1960年のLispで数学的に安全な記号として生まれ、1965年のALGOL Wで実装の便宜のために危険な参照へと姿を変え、1980〜90年代のStandard MLやHaskellが型理論で少しずつ手当てをし、2010年代のSwiftやKotlinがついにそれを主流言語の標準装備へと結実させました。ただし、「10億ドルの過ち」がすべて清算されたわけではありません。C、C++、そして後方互換を背負ったままのJavaのように、Null参照が今も現役の言語は少なくないのです。それでも今日あなたが書いた ?. の一行は、半世紀分の型理論がようやく手にした答えの一つです。皆さんは普段、Null安全の恩恵をどれくらい意識して書いていますか?

動画では各地層をやさしい対話形式で詳しく解説しています。ぜひあわせてご覧ください!

参考リンク

Hugo で構築されています。
テーマ StackJimmy によって設計されています。