型システムの相剋と棲み分け

檜山正幸 (HIYAMA Masayuki)
Tue Mar 15 2005:start
Wed Mar 16 2005:draft

構文的に定義される型と、オブジェクトに対して定義される型は、随分と違 う、むしろ正反対の特徴を持つことを指摘する。目的が違うのだから概念が 違ってもいいのだが、一方で、これらを統合したいニーズもある。

目次

1. はじめに

記事「XMLフレンドリーな型システム」 の第11節に、次のように書いた。

構文的型システムは(それができれば)、XMLとの相性はいいはずである。一 方で構文的型システムは、プログラミング言語の型システムとは相性が悪い。

ここで言っている「XMLと相性が良ければ、プログラミング言語とは相性が悪 い」てなことは、僕としてはサンザン言い尽くした気分になっている。が、 それは勝手にそんな気分に陥っているだけで、実際は、あまり明白に表明もし てないし、明確に説明もしてない(のだろう、たぶん)。

そこで、なるべく事情がクリアになるような説明をこの記事で与えたい。言 いたいことは、文字列やXMLによりうまく表現できるような型と、オブジェ クトの型(クラスやインターフェースだと思ってください)は、全然違うって こと。似てるところもあるのだけど、それよりは食い違いのほうが大きい。食 い違いをよーく認識して、それをどうやって乗り越えるかを考えなくてはなら ない。

僕が、「XMLと相性が良ければ、プログラミング言語とは相性が悪い」とサン ザン言い尽くした気分になっているのは、次のような理由からだ。1つは、 『JavaWorld』誌連載のなかで言及したつもりになっている。が、これはたぶ ん(僕の気持ちに比べれば)明白ではないだろう。もう1つ。 正しい プログラムを書くには 増補版テキストのなかでも触れている(*注1)。と、 僕は認識している。が、これも明確な説明にはなっていないみたい。

注1

ウウウウウ、このテキスト、半分しかできてない状態で放置してあるのだ。

で、最初から説明する。しかし、ネタは上記の素材から拝借している。いち いち引用元を明示はしない(自分で引用しているのだから、別にいいでしょ)。 また、記事「折れ線の例」「XMLフレンドリーな型システム」とも かぶっている。なお、この記事を読む前に 「XMLフレンドリーな型システム」は、ざっと眺めておいたほうがよいと思 うが、そんなに熱心に読む必要はない。

2. 例題の準備

例題は、幾何学的な点である(*注2)。点の座標成分であるスカラー型の選定で 既に問題を含んでいるが、この影響は今回考えたくない。概念的な実数、整数 に対して、double、longの型を使うとして、同じdouble、longがマークアップ でも使えるとする。

注2

カウンタ、スタックと幾何学的対象は、僕のお決まり(マンネリ)の例なん です。

クラスとしてのPointとLatticePointは次のように定義する(面倒だから publicは付けてない)。クラス名のお尻についてる「C」は不格好だが、後で 混乱を招かないように付けている(頭にCを付けたCPointのほうが好き?)。

/* 平面の点 */
class PointC {
 private double x, y;

 double getX() {
  return x;
 }
 double getY() {
  return y;
 }
 // ...
}

/* 平面の格子点 */
class LatticePointC {
 private long x, y;

 long getX() {
  return x;
 }
 long getY() {
  return y;
 }
 // ...
}

これに対応するマークアップの規則は次のように定義される。名前のお尻の 「M」は、クラスの「C」と同様、マークアップであることを示すための印。

PointM ::= <point x=double y=double />

LatticePointM ::= <point x=long y=long />

さらに、nonNegativeDoubleとnonNegativeLongは非負の数だとして、上半平 面にある点も定義しておく。

UpperPointM ::= <point x=double y=nonNegativeDouble />

UpperLatticePointM ::= <point x=long y=nonNegativeLong />

3. 構文的サブタイプ関係

前節で定義したクラスや要素パターンの背後には、もちろん本物の“幾何学 的な点”があるのだが、幾何学的な点に正面切って言及するのは避ける。幾何 学的な点は、気持ちの世界(メンタル・モデル)に存在するとしよう。

記号を無闇と増やさないために、PointM、LatticePointM、UpperPointM、 UpperLatticePointMを、構文的に定義される型の名前として使うことにする(*注3)。 これらの型の領域を、構文的な定義を満たす(パターンにマッチする)要素の 集合だとする。 「XMLフレンドリーな型システム」を読 んだかたは、話が食い違っていると思うだろう。そう、食い違っているのです! この記事では、意図されている意味/指示対象である“幾何学的な点”への言 及を避けている。よって、型の領域として“幾何学的な点”の集合をとるので はなくて、その表現であるXML要素の集合を採用する。これは既に、 構文的型システムに踏み込んでいる ことになる。構文的型システムについての説明は下のノートにある。

注3

記号の集合{PointM、LatticePointM、UpperPointM、UpperLatticePointM}は、 構文を定義するときの変数(非終端記号)の集合だが、それをそのまま、型シ ステムのソートの集合としても使うということ。

NOTE: 構文的型システム

PointCやPointMが平面内の任意の点を表し、 LatticePointCやLatticePointMが平面内の格子点を表すと考えるのは、何も悪 いことではない。だが、“平面内の任意の点”は、紙やホワイトボードの上に あるわけではないし、コンピュータの中にも存在しない。どこにあるかよくわ からないイデアルな存在を、正面から取り上げるのは止めよう、 というのが構文的型システムの基本発想である。だがこれは、心のなかに思い 描く“幾何学的な点”までも禁止するものではない。

幾何学的な点の代わりに、それを表現するときに使う構文的な対象、例えば、 文字列"(3, -1)"やXML要素<point x="3" y="-1" />を考える。「表現は対象を 指し示す」ものだが、指し示すべき対象が理念的だったり、曖昧だったり、そ もそも存在しないときには、表現を対象の代理に使う。いやっ、存在しないモ ノの代理というのも変だから、「表現のみを使う」と言うべきだろう。

ただし、表現には余計な自由度がある。例えば、"(3,-1)"と "(3,   -1)"は異なる文字列である(空白が違っている)が、点 の表現としては同じとみなさなければならない。そこで、表現には必ず正規形 を考える。例えば、「カンマの後に必ず1個だけのブランク文字を入れる」と いう正規形の規則があれば、次のように正規化される。

"(3,-1)" → "(3, -1)"
"(3,  -1)" → "(3, -1)"

正規形の表現は、事実上、値とみなすことができる。つまり、正規化写像を 意味写像とみなして、構文的型を通常の意味論の枠組みで扱うことも可能なの である。正規化さえキチンと定義すれば、なにも不便はない、というのが僕の スローガンである。

型PointM、LatticePointM、UpperPointM、UpperLatticePointMの領域を、そ れぞれ[PointM]、[LatticePointM]、[UpperPointM]、[UpperLatticePointM]と 書く。これらはすべて、要素の集合である(くどいが、幾何学的な点の集合で はない)。そして、これらの集合のあいだには次のような関係がある。

領域の包含関係を背景(根拠)として、型PointM、LatticePointM、 UpperPointM、UpperLatticePointMには、次のような型階層があるとしてよい だろう。

         PointM
         /   \
       /       \
LatticePointM  UpperPointM
       \       /
         \   /
     UpperLatticePointM

型PointMなどは構文的に定義される型であり、領域[PointM]などは「幾何学 的な点の集合ではない」と強調したが、それにもかかわらず実際は、構文 的対象(要素)と構文的集合(要素の集合=言語)が、幾何学的な点と点集合 をよく表している。つまり、構文的な包含により定義される構文的サブタイプ 関係(型階層)は、心のなかの幾何学的なモデル(*注4)とよく一致する。

注4

この「モデル」は、メンタル・モデルのことだと思っても、モデル論の用語 としてのモデルだと思っても、どちらでも通用する。

4. サブクラスはうまくいかない

今度はクラスについて考えよう。先に定義したPointCとLatticePointCという 2つのクラスはどんな関係にあるだろう。

クラスの内部状態空間(インスタンス変数の取り得る値の範囲)を、 [PointC]、[LatticePointC]と書くなら、事実として [LatticePointC] ⊆ [PointC]は成立している。これは、前節の[LatticePointM] ⊆ [PointM]と同 様な状況である。しかし、クラスの内部状態空間は外部から観測できるもので はない。[LatticePointC] ⊆ [PointC]を確認できる人は限られている(誰も 確認できないかもしれない)。

プログラミング言語が正式に認めた方法で、LatticePointCがPointCのサブタ イプであることを主張できないだろうか。LatticePointCをPointCからの継承 により定義すれば、明示的にサブタイプであると主張できる。だが、少なくと もJavaでは、LatticePointCをPointCを継承して定義することはできない。

上半平面の点を表すつもりのUpperPointCとなると、どうやってクラスを合理 的に定義すべきかさえ分からなくなる。例えば、次のコードでは、「上半平面 にある(y≧0)」という情報はコメントとしてしか現れない。

class UpperPointC {
 /* y >= 0 でなくてはならない */
 private double x, y;

 double getX() {
  return x;
 }
 double getY() {
  return y;
 }
 /* 以下、 y >= 0 を仮定したコード */
 // ...
}

UpperPointCをPointCから継承して定義することはできるが、それをしたから といって、継承のありがたみはない(*注5)。y >= 0 のチェックをはさむ必要がある から、ほとんどのメソッドを上書きする必要がある。

注5

継承の一番の現世利益は、差分プログラミングによりコード記述量が減るこ とだと思っているのだが、違いますかぁ?

さらに、クラスの型階層を次のようにしたいとしたら、どうだろう。

         PointC
         /   \
       /       \
LatticePointC  UpperPointC
       \       /
         \   /
     UpperLatticePointC

この図は、単一継承主義者はもちろん、多重継承主義者も嫌がるであろうダ イヤモンド形である。

5. サブクラスがうまくいくとき

Point, LatticePoint, UpperPointなどの概念を、型階層も含めてクラスとし て定義するのはうまくいかない。このテのサブタイプ概念を表現することは、 クラスが苦手とするところだ。

では、クラスが得意とするサブタイプ概念とはどんなものだろう。同じ PointCから出発しても、次の“色付き点”ならうまく継承で定義できる。

import java.awt.Color;

class ColoredPointC extends PointC {
 private Color color;

 Color getColor() {
  return color;
 }
 // ...
}

この例では、継承によるサブクラスは、基底クラスに対して、機能や特性を 追加している。Javaのキーワードextendsが暗示するように、継承により能力 の拡張/強化(エンリッチメント)が行われているのである。

6. 比較してみる

「LatticePointはPointのサブタイプである」も「ColoredPointはPointのサ ブタイプである」も、どちらも正しい主張だが、この2つの主張におけサブタ イプ概念は異なっている。

「LatticePointはPointのサブタイプである」ケースでは、LatticePointのイ ンスタンスは、自然にかつ一意的にPointのインスタンスと見なせる。つまり、 LatticePoint→Pointの方向に、自然な埋め込みが存在する。

「ColoredPointはPointのサブタイプである」のケースでも、ColoredPointを 自然にかつ一意的にPointと見なす方法がある。だが、それは埋め込みではな い。(x座標, y座標, 色)を(x座標, y座標)に対応させているから、色を忘れる 写像である。(1, 2, 赤)も(1, 2, 緑)も(1, 2)に対応するから、この写像は射 影なのである。

プログラミング言語、特にオブジェクト指向言語が得意とするサブタイプ概 念では、サブタイプ→スーパータイプの方向で射影が存在している。一方、オ ブジェクト指向言語が苦手とするサブタイプ概念では、サブタイプ→スーパー タイプの方向で自然な埋め込み(包含写像)が存在しているのである。射影と 包含は、逆(双対)な概念である。つまり、サブタイプ関係を支える背景が正 反対になっている。

7. 再びマークアップの例

ColoredPointの例をマークアップで考えてみよう。

PointM ::= <point x=double y=double />

ColoredPointM ::= <point x=long y=long color=Color />

プログラミング言語では、クラス継承を使って「ColoredPointはPointのサブ タイプ」だと言えた。では、上の要素パターンから、[ColoredPointM] ⊆ [PointM]が成立するかというと、全然ダメである。例えば、<point x="1" y="2" color="red" />は[ColoredPointM]に属するインスタンスだが、 [PointM]には属さない。

もし、[ColoredPointM] ⊆ [PointM]としたいなら、次のように定義しなくて はならない。

PointM ::= <point x=double y=double ** />

ColoredPointM ::= <point x=long y=long color=Color ** />

ここで、「**」は、任意の(ただし、名前がバッティングしない)属性が出 現してよいことを示す一種のワイルドカードである。ワイルドカードを許せば、 クラスの継承と似たようなサブタイプ関係が出現する(*注6)。これは、「属性だけを 持つ要素がレコード構造であり、オブジェクトもレコード構造である」という 類似性(*注7)に依拠している。

注6

[ColoredPointM] ⊆ [PointM]としたいだけなら、ColoredPointMにワイルド カードは必要ない。が、ワイルドカードを付けておかないと、色付きの点を拡 張して味付きの点とかにするときに、同じ困難に出会う。

注7

「オブジェクトもレコード」と言っているのは、データ(フィールド、イン スタンス変数)に注目して見れば、ということである。オブジェクトはレコー ドに過ぎないと言ってるわけではない。

いずれにしても、ワイルドカードを使えば、構文的な型階層がクラス階層と 似た振る舞いをすることは、良いニュースである。だがこれは、「どちらも所 詮はレコードだから」という状況がたまたまあったからで、過大に喜んではい けない。

8. インクルーシブな型階層とプロジェクティブな型階層

構文的な型は、文字列全体とかXMLインスタンス全体とかの広い領域から、目 的にふさわしい表現だけを切り出すことによって定義される。制限することが 型定義のための基本操作である。その結果、サブタイプの領域には、スーパー タイプの領域に向かう包含写像が付随することになる。つまり構文的な型シス テムでは、型階層は包含写像により支えられているといってよい。

一方、オブジェクティブな型は、共通機能だけを持った汎用(しかしプア) な定義からはじまり、それに機能や特性を付け加えることで新しい型が定義さ れる。サブタイプはスーパータイプよりもリッチだから、サブタイプの領域 (状態空間)からスーパータイプの領域には射影(または忘却)写像が存在す る。つまりオブジェクティブな型システムでは、型階層は射影写像により支え られている。

まとめれば、構文的な型システムは、包含(inclusion)による型階層を持ち、 オブジェクティブな型システムは、射影(projection)による型階層を持つ。 標語的には、「構文的な型システムはインクルーシブ、オブジェクティブな型 システムはプロジェクティブ」と言える。このことは、「階層がある」という レベルでは同じといえるが、階層を支える基盤は全然違うことを意味する。

9. 統合できるか

考えるべき問題は、基盤が全然違う型システムを統合できるのだろうか? だ。 この節では見通しを述べるが、曖昧でイイカゲンなことを言う。

構文的な型を定義する際の生成的な部分、つまり文法規則だが、これはオブ ジェクトのコンストラクタと解釈するのが自然だろう。文法規則は型構成規則 と解釈できるし、コンストラクタとは型構成子だから。

構文的な型の制限(条件)の部分は、現状のプログラミング言語に対応物を 見つけるのは難しい。仕様、制約記述、契約のレベルで、制限の記述を行うし かないような気がする。

僕としては、構文的な型システムを直接実装に落とすことは考えてない。 “構文的な型システム”が“仕様としての型システム”と対応すれば満足な ので、それならば手が届く範囲だと思っている。