PHP、Python、TypeScript、Rust。設計思想も歴史も違うこれらの言語が、この 10 年でよく似た機能を次々に取り込みました。enum、パターンマッチ、nullable 型、不変オブジェクト、async/await。偶然が重なったにしては揃いすぎています。
流行を追っているのでしょうか。そうではありません。共通しているのは機能ではなく、その機能が解こうとしている問題のほうです。
変化を並べてみる
言語ごとの差を脇に置いて、旧世代から現代への移行を並べると輪郭が見えます。
| 旧世代 | 現代 |
|---|---|
| null かもしれない値をそのまま渡す | nullable 型や Option<T> で欠如を明示する |
| 文字列定数で状態を表す | enum で許される状態を限定する |
| 何が投げられるか実装を読んで判断する | Result<T, E> や型付き例外で失敗を型に載せる |
if と switch を重ねて中身を確認する |
パターンマッチで型と構造を同時に照合する |
| オブジェクトを自由に書き換える | 既定を不変にし、変更箇所だけ明示する |
| 巨大なクラス階層から実装を継承する | 小さなインターフェースを合成する |
| コールバックを入れ子にする | async/await で逐次処理のように書く |
7 つとも別の機能に見えますが、方向は 1 つです。実行してみないと分からなかったことを、書いた時点で分かるようにする。
「記述量を減らす」ではない
この変化はしばしば「モダンな書き方は短くて読みやすい」と要約されます。この要約は間違ってはいませんが、原因と結果を取り違えています。
短くなった例はたしかにあります。PHP のコンストラクタプロモーションは 3 箇所に書いていた名前を 1 箇所にしました。Python の dict[str, str] | None は Optional[Dict[str, str]] より短い。
しかし、短くなっていない例も同じくらいあります。Rust で Option<User> を返せば、呼び出し側は match か unwrap_or を書く必要があり、null を返していた頃より行数は増えます。TypeScript で判別可能なユニオン型を定義すれば、素朴な any より宣言は長くなります。パターンマッチも、単純な分岐なら if のほうが短い。
それでもこちらが推奨されるのは、短さと引き換えに得ているものがあるからです。
得ているのは、表現できる状態の少なさ
?User という型を考えます。この型が取りうる状態は、User のインスタンスか null の 2 つです。一方 PHP 7.4 の型なし引数 $user が取りうる状態は、あらゆる値です。文字列も配列も来ます。
型を書くことは、取りうる状態の集合を狭める行為です。狭めれば狭めるほど、考慮すべき分岐が減り、テストすべき組み合わせが減り、他人が読んだときの前提が確定します。
enum も同じです。string $status は理論上すべての文字列を受け付けます。'acitve' というタイプミスも型としては正当です。UserStatus に変えた瞬間、状態は 2 つか 3 つに固定されます。減った分だけ、考えなくてよいことが増えます。
不変性も、可変性を捨てて「この値は途中で変わらない」という前提を買っています。async/await は、コールバックの呼ばれる順序という不定要素を捨てて逐次実行の見た目を買っています。
構文の変化は、書き手の手間と引き換えに状態空間を縮小する取引だった、と見るとすべて同じ話になります。
縮小した状態は、機械が検証できる
もう 1 段あります。状態空間を縮めるだけなら、規約やレビューでもできます。「status には active か suspended しか入れないこと」とドキュメントに書けば済む。
しかし規約は破られます。破られたことに気づくのは実行時か、運が悪ければ本番です。
構文に落とすことの本質は、縮めた制約を機械が検証できる形にすることにあります。enum にすれば、許されない値はコンパイラか実行時型検査が弾く。Result<T, E> にすれば、失敗の処理漏れをコンパイラが指摘する。Rust の match で enum にケースを足せば、対応が漏れている箇所を全部列挙してくれる。
人間が守る規約から、機械が確かめる制約へ。これが 10 年の移動の正味の内容です。
だから保証の強さが問題になる
この見方をすると、同じ機能に見えるものの評価軸がはっきりします。重要なのは書けるかどうかではなく、破ったときに誰がいつ気づくかです。
Python の型注釈は、mypy を通さなければ何も検査しません。TypeScript の型はコンパイル時に消え、実行時の値が型どおりである保証は別に必要です。PHP の型宣言は実行時に必ず効きますが、それはその行が実行されたときの話です。Rust の Option は、分解しなければそもそも値を取り出せません。
どれも「型を書ける」言語です。しかし、規約から制約への移動がどこまで進んでいるかは同じではありません。この差は言語の優劣ではなく、動的性をどこまで残すかという設計判断の結果です。
学び方への含意
言語をまたいで学ぶとき、記号を覚える方法は効率が悪くなります。?. と unwrap_or と ?? は表記が違うだけで、同じ問題を解いています。
代わりに、次の順で見ると言語の性格がつかめます。
- その言語は「欠如」をどう表すか
- 「失敗」をどう表すか
- 「状態の集合」をどう限定するか
- 「変わらないこと」をどう保証するか
- それぞれの制約を、いつ・誰が検証するか
5 番目が言語ごとの本当の差です。1 から 4 の答えはどの言語もよく似ていて、5 番目だけが大きく違います。
まとめ
- enum・パターンマッチ・nullable 型・不変性・async/await が各言語に揃って入ったのは流行ではなく、同じ問題への回答が収束した結果です。
- 変化の方向は 1 つで、実行時まで分からなかったことを、書いた時点で分かるようにすることです。
- 現代的な構文は必ずしも短くありません。短さではなく、表現できてしまう状態の少なさを買っています。
- 型を書くことは取りうる状態の集合を狭める行為で、狭めた分だけ考慮すべき分岐とテストの組み合わせが減ります。
- 本質は、縮めた制約を人間が守る規約から機械が検証する形へ移したことにあります。
- したがって言語を比較する軸は「書けるかどうか」ではなく「破ったときに誰がいつ気づくか」です。この点だけが言語間で大きく違います。