TypeScriptのanyみたいな「なんでも代入できる型」を分類する
安全でないもの - void * (C)
いろいろあるんだろうけど、Cから始めよう。void*
にはなんでもキャストできるし、元の型に戻すこともできる。まあany
。問題は変なことをやった場合。(Cはよくわからないから誰かが例を書いてくれ。)
キャストによって変な型が得られた場合の挙動は未定義なんだと思う。(知らないから、誰か仕様を調べてくれ。)未定義というのは、コンパイラとか計算機は何をしても良いという意味で、
- プロセスの実行を止める
- 最適化によってコードが消える
- プロセスのメモリを破壊しながら動き続ける
- 鼻から悪魔が出る
などのパターンがありうる。実際には大体SEGVで落ちると思うんだけど、それは仕様には含まれていない。
ここで「安全である」というのは「プログラムの実行中に未定義の状態にならない」という意味である。つまり、Cは安全ではない。
キャストの検査で安全になったもの - dynamic (C#)
動的型付けの言語が流行ったり、本質的に型を書くのが困難なパターンがあることが判明したりして、いくつかのプログラミング言語には「コンパイル時に型検査されない型」というのが導入された。C#で言うところの dynamic
である。
string x = ""; dynamic y = x; float z = y;
こんなプログラムはコンパイルできる(つまり静的な型検査が通る)が、実行してみるとz = y
でエラーが起きる。このことから、float
型の変数z
にstring
を代入して良いかどうかを実行時に検査していることがわかる。つまりz = y
のところでdynamic
からfloat
のキャストが挿入されていると考えて、キャストを実行時に検査していると解釈する。検査に失敗したらRuntimeBinderException
が発生する。
キャストに注目すれば、JavaのObject
とかもこのグループに入る(プログラマによる明示的なキャストは必要なので、dynamic
とはかなり書き味が違うけど、それはおいておくとして)。つまり、キャストは必ず実行時に検査されて、失敗したときになにが起きるかは言語によって定義されている。あるいは、C++のdynamic_cast
も、失敗を判定できて失敗したときの挙動が定義されているという観点でこちらに入る。
さて、キャストの失敗でなにが起きるのかは定義されるようになったのだから、未定義ではない。つまり(少なくともこの面において)C#やJavaは「安全」になった。
安全なんだけど検査が省略されるもの - any (TypeScript)
dynamic
までだと話が綺麗なんだけど、TSはまた厳しいところをついていて、ちょっと難しい。
TSではany
から他の型へのキャストは実行時に検査されない。じゃあ安全じゃないのかっていうと、安全である。つまり、JavaScriptとして実行されるので、キャストの失敗を見過ごすことによって発生する問題は、どちらにしてもJSのランタイムによって保護されているのだ。どうせ安全なんだから、キャストの際の型検査を省略しても良いという発想。これは、かなり強烈な性質で、Java以降のプログラミング言語にかなり広く共有されている「キャストのような静的な型検査では問題を検査できない操作には、実行時のチェックで補完しよう」という戦略から離れていることを意味する。すごい。
逆に言うと、この「キャストの検査」を諦めたことによって、TypeScriptではかなり柔軟な型が書けるようになっている。仮に、キャストの検査をやる方向でデザインするとすると、JSのランタイムで表現できる型しか使えないことになるので、けっこう厳しい。最近はクラスが入ったので良いんだけど、少し前まではobject
とnumber
とstring
とarray
と……くらいしかなかった。「この式の型はobject
です(number
とかstring
ではないけど、どういうメソッドがあるのかはわかりません)」という型付けは、あまり実用的ではない。TSでは、オブジェクトの型もかなり詳細に書けるようになっていて、まあ現実的ではない感じのランタイム型検査になる。(もちろんやればできる。 )
そういうわけで、any
からのキャストで検査を省略するというのはまあ現実的な判断だと思うんだけど、意外性はあるのだった。
ちなみに、any
がなくてもTSの配列はcovariantなので、その点で型検査は健全ではない。
const xs: string[] = [] const ys: (string | number)[] = xs // 本当はこの代入ができてはいけない ys.push(1) const s: string = xs[0] // 本当はnumber
(どうせ一回any
を介せば代入できてしまうんだから、まあ気にせずにcovariantにするのが正しいのだろう。)
宣伝
特にここで書いた中ではTypeScriptに関係が深いのですが、「動的型付けの言語に型を付けようとする人類の試み」について一回話がしたいなーと思っていて、せっかくなので富山Ruby会議でやることにしました。
いろいろ人類が試行錯誤した歴史を私が理解している限りで解説して、「じゃあRuby (Steep) でどうすんの?」という話をします。みなさまふるってご参加ください。