2006年06月18日

妄想型プログラミング言語

 経営者がビッグなビジネスを妄想するように、プログラマは自分の設計した言語を妄想する。どちらも、なんの役にも立たないが、やめられない。
 この無益な行為のなかで、面白い言語機能を思いついた。ここにメモしておく。

public string Func()
{
using (FileStream fs = new FileStream("test.txt", FileMode.Open))
{
using (StreamReader sr = new StreamReader(fs))
{
return sr.ReadToEnd();
}
}
}

 よくあるC#のコード片だ。
 このコード片には、意味的な無駄が多い。
 まず、もしC#に型推論があれば、こう書ける。

public string Func()
{
using (var fs = new FileStream("test.txt", FileMode.Open))
{
using (var sr = new StreamReader(fs))
{
return sr.ReadToEnd();
}
}
}

 FileStreamだのStreamReaderだのと2回書かせるのは、カナ坊の陰謀としか思えない。
 ここで重要なのは、fsもsrもその型はコンパイル時に決定している、ということだ。コンパイラは右辺の型でvarを置き換えているにすぎない。これと同様に以下の話も、実行時ではなくコンパイル時の話である。
 さて、まだ無駄がある。fsとsrだ。これらの変数名はそれぞれFileStreamとStreamReaderを省略したものだ。綴りを変えてあるだけで意味的にはこれもやはりカナ坊言葉だ。
 実は私はこのことで、かれこれ10年ほど悩んでいた。
 私は、「よい名前(クラス名、関数名、変数名、名前空間名)のつけかた」を余すところなく説いた記事や本を10年以上探してきた。しかし、ある程度までのことは書いてあるものの、余すところなく、とまでのものには出会えなかった。やがて私は気づいた。これは、ある程度以上から先は、暗黙知でしかありえないことなのだと。
 さらに進んで、こういう結論に達した――プログラミングから計算機科学を引き算すると、「よい名前」が残る。
 設計や言語のよさは、よい名前をもたらすかどうかで測られる。よい名前についての暗黙知を持たないプログラマは、よいプログラマではありえない。つまるところ、「よい名前」こそプログラミングの極意だ。
 サンプルコードは概して、よい名前を使わない。たとえば上のコード片では関数名がFuncになっている。実際のコードではこんな関数名は使わない。
 だから私は悩んだわけだ。fsだのsrだのといった変数名は、Funcという関数名と同様、サンプルコードの世界からの悪しき流入であり、本当はもっとよい名前をつけられるのではないか?
 だが私はついに発見した。これもやはりカナ坊の陰謀だ。こう書けるべきなのだ。

public string Func()
{
using (var = new FileStream("test.txt", FileMode.Open))
{
using (var = new StreamReader(FileStream))
{
return StreamReader.ReadToEnd();
}
}
}

 ある変数スコープ内で、ある型の変数が1個しかありえないことが自明である、というケースは非常に多い。変数名のかわりに型識別子でバインドされる変数があっていい。上の例では、変数宣言から変数名を省略することで、そのような変数を宣言している。
 こういう変数を、仮に「型バインド変数」と名づける。

 クラスのメンバ変数にも型バインド変数は必要だ。ただしこの場合、メンバ関数内では、メンバ変数と同じ型あるいは親子関係にある型の型バインド変数の宣言を禁止すべきだ。つまり、こういう書き方はコンパイルエラーになるべきだ。

class MyClass
{
FileStream;

public string Func()
{
using (var = new FileStream("test.txt", FileMode.Open))
{
using (var = new StreamReader(FileStream))
{
return StreamReader.ReadToEnd();
}
}
}
}

 親子関係にある型も禁止、つまり、どちらのFileStreamをStreamに置き換えてもやはりコンパイルエラーになるべきだ。
 型バインド変数は、ある変数スコープ内である型の変数が1個しかありえないことが自明、というケースのためのものだ。変数スコープが上書きされるケースや、親子関係にある型が2つ出てくるケースは自明とはいえず、型バインド変数の出番ではない。(なお、型バインドのメンバ変数に代入するときは、左辺をthis.FileStreamと書く)

 これだけで話が終われば平和で些細な言語機能なのだが、実はお楽しみはこれからだ。
 型バインド変数はその変数スコープと特殊な関係にある。そして、上のコード片で、ReadToEnd()というメンバ関数がある型バインド変数はStreamReaderだけだ。
 だから、こう書けていい。

public string Func()
{
using (var = new FileStream("test.txt", FileMode.Open))
{
using (var = new StreamReader(FileStream))
{
return ReadToEnd();
}
}
}

 つまり、関数の名前空間に、thisのメンバ関数だけでなく、型バインド変数のメンバ関数も含める。これを仮に、「関数名前空間の拡張」と呼ぶ。
 阿鼻叫喚の渦を巻き起こす超Perl級の言語機能に見える。しかし角を取る方法はいくらでもある。変数宣言時にimplicit指定子を要求するなどだ。それに、フールプルーフは実際に馬鹿を観察しないことには論じられない。先に進もう。
 ローカル変数と同様、型バインドのメンバ変数のメンバ関数も、名前空間に含まれる。ちょっとクラス継承(実装継承)に似た働きをするわけだ。ではもっとクラス継承らしくしてみよう。以下のようなキャスト演算子を、デフォルトで定義するのだ。この機能を仮に、「デフォルトのキャスト演算子」と呼ぶ。

class MyClass
{
public FileStream;
...
public static implicit operator FileStream(MyClass mc)
{
return mc.FileStream;
}
}

 一目瞭然だが、このMyClassのインスタンスをFileStreamにキャストして得られたインスタンスは、MyClassに戻すことはできない。しかし、そもそもダウンキャストは型システムの抜け道だ。そのためC#でもJava言語でも、ダウンキャストは明示的に指定しなければならない。ダウンキャストを前提に設計することは滅多にない。
 ダウンキャストがどうしても必要なら、以下のように書ける。これは、ダウンキャストをデフォルトで提供するよりは、ずっと合理的なはずだ。

class MyClass
{
public MyFileStream;
class MyFileStream : FileStream
{
public MyClass;
public MyFileStream(string, FileMode, MyClass mc) : base(string, FileMode)
{
this.MyClass = mc;
}
}
}

 ダウンキャストを例外として考えると、こんな考えが浮かぶ――こうしてみるとクラス継承のほうがむしろ、言語機能として一般性に欠けるのでは?
 そもそもクラス継承を悪だとする議論は古くからある。この煽りを食って、Java VMと.NET CLRは多重クラス継承ができなくなってしまった。単一クラス継承が残されたのは、必要悪ということだろう。多重クラス継承が悪で単一クラス継承が善なのではない。「複数は耐えがたいが1個なら耐えられる」という妥協が、単一クラス継承の本質だ。
 しかし私に言わせれば、多重クラス継承をなくしたことで、重要なものまで失われた。デザインパターンでいうコンポジションで、委譲のためのコードを書いたことがある人なら、わかるはずだ。委譲のためのコードとは、およそ非人間的なコピペだ。
 では、そもそもクラス継承のメリットを、継承以外の手段で提供すればいい。型バインド変数&関数名前空間の拡張&デフォルトのキャスト演算子なら、それができる。

 まとめ。
 ある変数スコープ内で、ある型の変数を1個しか使わないことが自明、というケースがある。そのような変数のために、変数名のかわりに型識別子でバインドするという言語機能を考える。そのような変数を、型バインド変数と呼ぶ。
 型バインド変数は、ローカル変数だけでなく、メンバ変数としても宣言できる。
 関数の名前空間に、型バインド変数のメンバ関数を含める。この機能を、関数名前空間の拡張と呼ぶ。
 型バインドのpublicなメンバ変数があるクラスは、その型へのキャスト演算子をデフォルトで備える。キャスト演算子を使うことで、その型でバインドされているメンバ変数が得られる。この機能を、デフォルトのキャスト演算子と呼ぶ。
 型バインド変数&関数名前空間の拡張&デフォルトのキャスト演算子によって、クラス継承の機能を肩代わりできる。これはクラス継承の問題点を避けながら利点を得られ、さらに利点をより使いやすくしている。

 さて、こんな機能が実装された言語を使える日は来るのか? ありえない。
 というわけでこれを、「妄想型プログラミング言語」と名づける。

Posted by hajime at 2006年06月18日 06:08
Comments