某エントリが型について再考するきっかけになったのは事実だが、個々人の思想の成否を問う気がないのでとくにリンクはしない。ここでは型とは何かという点について僕なりの思想を記しておきたい。
データ型を区別しない世界
ごくシンプルなチューリングマシンを考えよう。
チューリングの仮想機械は、
無限に長いテープ
その中に格納された情報を読み書きするヘッド
機械の内部状態を記憶するメモリで構成され、内部状態とヘッドから読み出した情報の組み合わせに応じて、次の動作を実行する。
ヘッド位置のテープに情報を書き込む
機械の内部状態を変える
ヘッドを右か左に一つ移動する上の動作を、機械は内部状態が停止状態になるまで反復して実行し続ける。
チューリングマシン
この原始的な世界において「型」はない。メモリは抽象的で全てのメモリは同等に扱われ区別する必要はない。
また、チューリングマシンに程近い原始的なプログラム言語であるBrainfuck系も同様にデータに型の区別をしないし出来ない。Whitespaceもそうだ。
例えばBrainfuckで以下のバイト列をメモリに書きだしたとする。(16進数)
e3 81 82
このバイト列はUTF-8で「あ」を表現している。また、メモリの別の位置に次のバイト列を書きだそう。
50 00 00 00
これはエポック秒で2012年7月13日 20:01:20 を表現している。この2つのバイト列は相互に演算することができる。
50 e3 81 82
このデータは何か?UTF-8として解釈すれば「Pあ」であり、エポック秒として解釈すれば2013年1月2日 09:38:26となる。問題はこれを望むのかどうかだ。UTF-8を表現したバイト列とエポック秒を表現したバイト列を区別して、混ぜないようにし、その扱い方を変えたいのかどうか。
型というものを考える動機付け、それは「区別したい」ということ。データ型を区別しようとした時、そこに概念としての型が産まれる。
概念としての型
このUTF-8のバイト列とエポック秒のバイト列はBrainfuckの言語機能としては等価であって、なんらの区別がされない。しかし、僕らがプログラムを組む上ではこれらは決して混ぜてはいけない情報であるし、うっかりして混ぜないための工夫をこらすことになる。いや、Brainfuckでプログラムすること自体がないかもしれないが。
さて、今度はJavaの例を挙げよう。以下の2つの変数はいずれもString型だ。
String a = "<p>Hello World!</p>"; String b = "<p>Hello World!</p>";
変数aもbも、java.lang.String型*1であって、相互に代入可能な同じ型である。だが、この2つの文字列は混ぜてはいけない。aはHTMLのタグで、bはそれをエスケープしたものだ。
これは、言語機能としての型は同一だが混ぜてはいけないモノの一例だ。このように、比較的強い型付けをもつJavaを引き合いにしても言語機能の型と概念としての型は容易に乖離する。
型の相互変換
概念としての型(と僕が呼んだもの)は、このように扱いを区別したいデータが存在した時点で自然発生するというのが僕の立ち位置だ。
データに型という区別をつけたが、これらを相互に変換したくなったり演算したくなったりすることは当然ある。数値型を元に「A001」「A002」といった型番を作りたいだとか。先に挙げたUTF-8のバイト列とエポック秒のバイト列というのはおよそ相互に変換することのない例として挙げたものだが、比較的近い位置にあって相互変換したくなったり演算したくなる型というのは多くある。
ここで抑えておきたいのは、原理的に相互変換が不可能なケースが多くあるということ。型の変換によってデータが欠損するケースだ。
例えば論理値 true と false を数値型の1と0を使って表現することが出来る(論理値型→数値型の変換)が、逆に数値型から論理値型に変換しようとすると2とか3とかどうするの?ということになって0はfalse、他は全部1というようなデータ欠損のある変換方式をとることになる。(数値型→論理値型の変換)
数値型から論理値型へ、それをまた逆に数値型へとした場合、データは欠損して元に戻らない。型と型を変換する場合、完全な全域写像となるケースはごく稀であって、そのほとんどは欠損を伴う。
型の変換を意識することのない環境とは
概念としての型、言語機能としての型、そしてその乖離、相互変換の不完全性と挙げてきた。
真に「型がない」プログラム言語は存在可能なのだろうか。Brainfuckのような言語ですら、扱うバイナリ列に区別をもたせようとした時点で概念としての型が生まれ、意識して区別せねばならなくなることを挙げた。Brainfuckはチューリング完全なのだから、僕らが普段扱う高級言語のコンパイラもランタイムも実装可能なハズである。この間に線を引いて、ここまでなら型がなく全てが相互変換可能だ!となりうるとは思い難い。
いや、あるいは同じビット列に対してそれぞれ違う意味をあてて、利用箇所に応じてそれを区別しようとするから型が産まれくるのではないか。全てのデータに違うビット列を割り当てたならば単一の型で万物を処理できるのではないか――そう、すべての文字を集め一意なコードポイントを振って文字エンコーディングの概念をなくそうとするUnicodeのように――。
ここには「型を内包してるってそれは型じゃないのか」とツッコミを入れて下さい。
どうにも、扱う問題領域が単一種のデータに限られるという条件下でしかやはり型は意識せざるを得ないようだ。
まとめ
- データの扱い方に区別が必要となったらそこに概念としての型が産まれる
- 言語機能はその区別に便宜を図ってくれるが用途に合うかどうかはシチュエーションによる
なすべき区別がなされないのはバグの元であるからヒューマンエラーが入り込まない工夫が欲しいと個人的には思っている。