レコードデータの型階層(type hierarchy)は、ときおり考えてみる問題なのです。毎回考えた結果を忘れている(苦笑)ような気がするので書き留めておくことにしよう、っと。JavaScriptを事例に考えます。ただし、現状のJavaScriptの型システムは奇妙で不格好なところがあるので、JavaScript 2.0(ECMAScript 第4版)をベースにします。
[追記]id:mal_blueさんより、上に「JavaScript 2.0(ECMAScript 第4版)」として参照したURLは、現在では歴史的意味しか持たないことをご指摘いただきました。(コメント欄参照)。したがって、以下の説明で引用されている型システムが次期JavaScriptにそのままの形で取り入れられるわけではありません。
しかし、次期JavaScript仕様がどうなるかは、このエントリーと引き続くエントリーの内容に直接の影響はないので、この追記以外の修正はしません。次期JavaScriptの仕様そのものに関しては、コメント欄に記述されているURLを参照してください。[/追記]
内容:
- 値の型
- レコード型
- 未定義(undefined)値を使ってみる
- それじゃどうする
●値の型
今のJavaScriptでは次のような変なことが起こります。
js> typeof 5
number
js> typeof new Number(5)
object
js> typeof "hello"
string
js> typeof new String("hello")
object
js>
JavaScript 2.0では、このへんは整理され、次のようなデータ型が導入されるようです。
データ型 | 説明 |
---|---|
Never | 値はまったく無い |
Void | 値はundefinedだけ |
Null | 値はnullだけ |
Boolean | 値はtrueとfalse |
Number | +0.0, -0.0, +∞, -∞, NaNを含む数値 |
String | 文字列、nullを含む |
参考:
JavaScript 2.0にない概念ですが、上に挙げた型の値をすべて含むような型をAnyValue型としましょう。これらの型を値の集合と解釈するなら、集合の包含関係は次のようになります。
AnyValue
/ | \\___
/ | \ \
Boolean Number String Void
\ | | / /
\ | Null/ /
\ | // /
Never__/
●レコード型
JavaScriptでは、{name: "板東トン吉", age: 27}
のようなデータをオブジェクトと呼びますが、ここではより一般的な用語を使ってレコードと呼びましょう。nameとageはレコードのフィールド名です。
フィールド名がfoo, barであるようなレコードの全体を Record{foo, bar}と書きましょう。先に例に挙げたレコードは、Record{name, age}に含まれることになります。各フィールドの型を制限したいことも多いので、Record{name: String, age: Number}のような書き方も許します。Record{name: String, age: Number}は、パブリックな型付きフィールド(だけ)を持つクラス宣言だと思ってよいでしょう。あるいは、レコードに対するスキーマとも言えます。フィールド型を書いてないRecord{name, age}は、Record{name: AnyValue, age: AnyValue}とみなすのが妥当でしょう(議論の余地はありますが)。
以下、話を簡単にするため入れ子のレコードは考えません。つまり、フィールドの型は、先に挙げたNever, Void, Null, Boolean, Number, String, AnyValueのどれかに限るとします。
●似ているようでも何の関係もない
データ{name: "板東トン吉", age: 27}は、レコードの集合Record{name: String, age: Number}に入るのは明かです。しかし、データ{name: "板東トン吉", age: 27, married: false}はRecord{name: String, age: Number}に入りません。余計なフィールドmarriedがあるからです。{name: "板東トン吉", age: 27, married : false}は、Record{name: String, age: Number, married: Boolean}になら入ります。
以上のこと整理しておきましょう。
- 以下 ton1 = {name:"板東トン吉", age:27} とする。
- 同様に、ton2 = {name:"板東トン吉", age:27, married : false} とする。
- Person1 = Record{name: String, age: Number} とする。
- Person2 = Record{name: String, age: Number, married: Boolean} とする。
このとき:
- ton1 ∈Person1
- ton2 !∈Person1 (!∈ は「所属しない」こと)
- ton2 ∈Person2
なんとなく、Person2 ⊆ Person1 のような気がするかもしれませんが、そんなことはありません。もし、Person2 ⊆ Person1 なら、ton2 ∈Person2 から ton2 ∈Person1 のはずですが、上の事実が反例を与えています。では、逆に Person1 ⊆ Person2 なのかな? それも違います。それどころか、データ集合Person1とPerson2は一切何の関係もありません。事実は、
- Person1 ∩ Person 2 = 空集合
です。
「えっ? そんなはずはない」と思われるでしょうね、その疑念の背景には、次のような“感覚”があるのでしょう。
// 構文はJava風class Person1 {
public String name;
public Number age;
}class Person2 extends Person1 {
public Boolean married;
}
上のような“感覚”は自然なのかもしれませんが、この“感覚”をうまく説明するのはけっこう面倒なのです。安直素朴な定式化はすぐに破綻します。
●未定義(undefined)値を使ってみる
ton1 = {name: "板東トン吉", age: 27} にはフィールドmarriedが存在せず、ton2 = {name: "板東トン吉", age: 27, married: false} にはフィールドmarriedが存在するので、この2つのデータはまったく別物になっています。しかし、実際のJavaScriptでは、ton1のフィールドmarriedにアクセスできます。
js> var ton1 = {name:"板東トン吉", age:27}
js> ton1.married == undefined
true
js>
つまり、フィールドmarriedが存在しないのではなくて、存在するが値がundefinedになっているだけ、と考えることができます。marriedの値がundefinedであるようなレコードのスキーマは次のように書けます。
- Person1' = Record{name: String, age: Number, married: Void}
{name: "板東トン吉", age: 27} ←→ {name: "板東トン吉", age: 27, married: undefined} という対応で、Person1とPerson1'のデータは1:1に対応が取れます。
しかしですね、こうしてみても事情は改善しないのですよ。確かに、Person1とPerson1'は1:1対応で関係付けられますが、Person1'とPerson2はやっぱり何の関係もありません。「明示的に書いてないフィールドの値はundefinedにする」という細工は分かりやすいのですが、整合的型システムの構成には役に立ちません。
●それじゃどうする
直感的には、Person1とPerson2は関係しそうですが、値の集合としては何の関係もない(共通部分は空集合)のです。特殊値undefinedを使う細工も本質的な改善にはなってません。
じゃ、どうやってレコード型(例えばPerson1とPerson2)のあいだに型階層関係を導入したらいいのでしょうか? それは次回に考えます(今日は疲れたので、「続く」)。