このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

レコードの型階層を合理的に説明できるか その3

前回(その2)から引用:

“未定義の定義”ごとに、型や型階層の概念が少しずつ違ってきます。それらは並列に存在する異なった定式化であり、優劣を論じても生産的ではありません。無理に優劣を付けようとすると、不毛な議論になるわけです。

というわけで、「未定義とは何か」を中心に話を進めます。

ton1 = {name: "板東トン吉", age: 27} のとき、ton1.marriedは“未定義”です。この状況の取り扱い方として、次のような処方が考えられます。

  1. ton1.married という式は構文エラーとして前もって排除する。
  2. ton1.married はどんな値にもなり得ると考える。
  3. ton1.married はどんな値でもないと考える。
  4. ton1.married はある特別な値であると考える。

1番目の考え方は話題にしません。残りの3つを扱います。4番目は現状のJavaScriptの方法と似てはいますが、後でJavaScriptとは異なった発想を紹介します。

これらの“未定義の定義”を導入して比較する際に、インスタンススキーマ/一般レコードという道具立てがあると便利なので、その説明を先にします。

内容:

  1. レコードのインスタンススキーマ
  2. インスタンススキーマの統合:一般レコード
  3. インスタンススキーマの一般レコードへの埋め込み
  4. 一般レコードの包摂順序
  5. 埋め込みと包摂順序を使ってみれば
  6. もう少し述べるべきこと

●レコードのインスタンススキーマ

今までレコードデータと言ってきたものを、ときに(レコードの)インスタンスとも呼びます。{name: "板東トン吉", age: 27}や{name: "板東トン吉", age: 27, married: false}はインスタンスですね。それに対して、{name: String, age: Number}のような形を(レコードの)スキーマと呼びましょう。インスタンススキーマは形がソックリですが、次の点が異なります。

  • インスタンスでは、フィールド名の後にが記述される。
  • スキーマでは、フィールド名の後にが記述される。
  • (常識的な意味で)解釈が違う。

既に使っている記法 Record{name: String, age: Number}は、スキーマ{name: String, age: Number}に適合するレコードの集合ということになります。(スキーマに適合する集合はイッパありますが、とりあえす気にする必要はありません。)

インスタンススキーマの統合:一般レコード

レコードのインスタンス{name:"板東トン吉", age:27} と、レコードの集合/型を定義するスキーマ{name: String, age: Number}は形が似ています。いっそのこと、インスタンススキーマも両方含むような広い世界を考えてみてはどうでしょうか。

試みに、次のような抽象的(実装を伴わない)データ構造を考えます。

  1. 任意のフィールド名に対して“値”が対応している。(全域性)
  2. フィールド名に対する“値”は単一値ではくて値の集合である。(集合値)

このようなデータを仮に一般レコード(universal record)と呼びましょう。形式的に定義すれば、フィールド名として許される名前の全体をNameとして、Name→Pow(AnyValue) という写像が一般レコードです。ここで、AnyValueはすべての値の集合、Pow(X)はXのベキ集合(Xの部分集合全体の集合)です。

任意の一般レコードを書き下すのは(フィールドも値も、ものすごくたくさんあるので)困難ですが、我々が興味のあるのは、少数*1のフィールド以外は一定値をとるものなので、例えば次のように表記できます。

  • {name: String, age: {21, 27}, married:Boolean, *: {undefined}}

最後に付いた星印「*」が「その他のフィールド」を意味します。この例では、name, age, married以外のフィールドの値はすべて{undefined}です。

レコードのインスタンススキーマ、そして今導入した一般レコードはよく似ています。が、同じではありません。インスタンススキーマを一般レコードの世界に埋め込んで考えたいのですが、この埋め込みは色々ありえます。埋め込みの仕方で後の議論も変わってきます

インスタンススキーマの一般レコードへの埋め込み

インスタンススキーマを一般レコードの領域に埋め込む方法で、一番扱いやすいのは次の方法(ルール)でしょう。(「扱いやすい」とは僕の印象で、絶対的な判断ではありません。)

インスタンスの埋め込み:

  • 単一の値は、その値だけを持つ単元集合に直す。
  • 明示的に書いてないフィールドの値はNever(空集合{})とする。

スキーマの埋め込み:

  • 明示的に書いてないフィールドの値はAnyValueだとする。

具体的な事例を挙げれば:

  1. {name:"板東トン吉", age: 27} → {name:{"板東トン吉"}, age: {27}, *: Never}
  2. {name:"板東トン吉", age: 27, married: false} → {name:{"板東トン吉"}, age: {27}, married: {false}, *: Never}
  3. {name: String, age: Number} → {name: String, age: Number, *: AnyValue}
  4. {name: String, age: Number, married: Boolean} → {name: String, age: Number, married: Boolean, *: AnyValue}
  5. {name: String, age: Number, married: Void} → {name: String, age: Number, married: Void, *: AnyValue}

まずはこの方法、つまり、未定義のフィールドの値は「インスタンスではNever、スキーマではAnyValu」とするルールを調べてみます。

●一般レコードの包摂順序

xとyが一般レコードであるとき、ある種の順序(記号「⊆:」で表す)を次のように定義します。

  • x ⊆: y とは、どんなフィールド名aに対しても、x.a ⊆ y.a なこと

以下に補足的説明。

  1. 記号「⊆」は、集合の包含関係です。
  2. 一般レコードでは、フィールド値は集合なので、「x.a ⊆ y.a」は意味を持ちます。
  3. 一般レコードは、任意のフィールド名に対する値(集合値)が定義されているので、「どんなフィールド名aに対しても」という表現も有効です。
  4. 記号「⊆:」で表現される関係は集合の包含関係と似てますが、同じではないので違う記号(コロンが付いている)にしました。

「⊆」と「⊆:」を区別したのと同じ理由で、上のように定義された一般レコードにおける順序関係を包摂<ほうせつ>順序と呼ぶことにします。一般レコードとその上の包摂順序は非常に便利です。所属関係(∈)や包含関係(⊆)では説明できない現象を包摂順序(⊆:)を使うとうまく取り扱えます。

●埋め込みと包摂順序を使ってみれば

インスタンスton1とton2、スキーマPeson1とPerson2を次のものだとしましょう。

  • ton1 = {name: "板東トン吉", age: 27}
  • ton2 = {name: "板東トン吉", age: 27, married: false}
  • Person1 = {name: String, age: Number}
  • Person2 = {name: String, age: Number, married: Boolean}

Person1やPerson2をレコードの集合と解釈すると:

  • ton1 !∈ Person2 (「!∈」は「所属しない」)
  • ton2 !∈ Person1

でした。この事実は、レコードを「公開フィールドだけのクラス」とみなしたいときには、たいへん具合の悪い結果です。

さて、インスタンススキーマを先に述べた方法で一般レコードとみなしたものを「'」を付けて示しましょう。つまり、

  • ton1' = {name: {"板東トン吉"}, age: {27}, *: Never}
  • ton2' = {name: {"板東トン吉"}, age: {27}, married: false, *: Never}
  • Person1' = {name: String, age: Number, *: AnyValue}
  • Person2' = {name: String, age: Number, married: Boolean, *: AnyValue}

表に書いたほうがわかりやすいかな。

データ name age married その他
ton1' {"板東トン吉"} {27} Never Never
ton2' {"板東トン吉"} {27} {false} Never
Person1' String Number AnyValue AnyValue
Person2' String Number Boolean AnyValue

この表を眺めて、⊆: の定義を参照すれば、次のことがわかります。

  • ton1' ⊆: Person2'
  • ton2' ⊆: Person1'

これは望ましい結果です。さらに、次の関係も容易にわかります。

  • ton1' ⊆: ton2' ⊆: Person1' ⊆: Person2'

一般レコードの世界では、インスタンススキーマも、すべての関係を包摂関係⊆:で捉えることができます。

●もう少し述べるべきこと

インスタンススキーマも一般レコードの世界に埋め込めて、そこでは一様な扱いができます。しかし注意していただきたいのは、インスタンススキーマが完全に同じ扱いにはなっていないことです。例えば、中身のない形 {} はインスタンスともスキーマとも見なせますが、インスタンスとして埋め込めば{*:Never}と解釈され、スキーマとして埋め込めば{*:AnyValue}と解釈されます。

インスタンスは外延(個体とその集まり)、スキーマは内包(条件、制約)なので、鏡に映った像のように「似てはいるけど逆である」ような関係になっています。一般レコードの世界は、実体と鏡像が共に(対等な権利で)棲める世界なわけです。

さて、インスタンススキーマを一般レコードの世界へ埋め込む方法は他にもあります。公平さや相対化のために、他の方法も検討するべきだと思うので、(たぶん)あと1回「続く」。

*1:Nameは通常無限集合です。ここで「少数」とは有限個を意図しています。