プログラミング言語 Cecil について、 唐突に学び始めたのでメモを書き殴るページ。 本当に学びながら書いているので、たぶん嘘吐きまくっていると思います。 間違いの指摘大歓迎。
(※2003/02/06追記。2002年12月に、UW Cecil/Vortex 3.1 Release が出て ページURLが変更されたため、リンクを修正しました。このリリースは、 Cecilインタプリタが単独配布されるようになるなど、ちょっと遊んでみたい人には 嬉しい改良です。)
(※2003/12/17追記。某所からのリンクをたどって自分の書いたこの記事を客観的に 読み返してみると、試行錯誤してるのはわかるけどオマエ全然理解できてねーよ、 って感じですね。文法の概観には便利ですが…。書き直すかなぁ。)
UW Cecil/Vortex 3.0 のWindows版を使う。何だか Java,C++,SmallTalk に更に Modula-3 など用のフロントエンドが用意された中間言語コンパイラなようで、 色々楽しめるようだ。しかしここはとりあえずCecilに限って弄ってみることとしよう。
ダウンロードして解凍したら、特に何もせずそのままインタプリタが起動できる。 Cygwinが要ると書いてあったが既にインストール済みなので楽だったのかもしれない。
Cecil> print_line( "Hello, World" );
Hello, World
まぁ、ここはごく普通。
Cecil> let a := 1;
Cecil> let b := 2;
Cecil> a + b;
3
変数定義とか。演算子とか。これも特に違和感はない。
ただし上のように書くとaやbという変数自体は(変数の指すオブジェクトは、
ではなく)変更不可になるので、書き換え可能な変数にするときには
明示的に var
と付けて宣言する必要があるらしい。
Cecil> let a := 1;
Cecil> a := 2;
eval error: assigning to a, a constant
Cecil> let var c := 3;
Cecil> a + c;
4
Cecil> c := 4;
Cecil> a + c;
5
実はマニュアルの Object and Inheritance という第一章は、 サンプルソースの動かし方がよくわからなかった。 ので飛ばして先を読んでみたのだが、 次回はその辺のオブジェクト指向部分をマジメに読んでみよう。
Cecil> method gcd( a@:integer, b@:integer ):integer {
> if( a=b, {a}, {if( a<b, {gcd(a,b-a)}, {gcd(a-b,b)})} );
> }
Added method gcd(@integer, @integer) (C-interpreted)
Cecil> gcd(100,44);
4
まぁ、見たまんまというか。ただし標準ライブラリの method の見様見真似で書いてるだけので、引数の型(?)指定に @: を使うべきなのか @ を使うべきなのか、意味も違いもわからない。 {...} で Closure を作ったりif/whileが(予約語でもSpecial Formでもなく) 関数だったりする言語は個人的に非常に好きなので好感度120%アップなのですが、 しかしこの構文でそれをやるとif-then-elseがやたら読みにくい感じが。 あまり使わないのだろうか?
Cecil> 100.gcd(44);
4
FreeFunctionとMethodの違いは特に無い様子。 ドットを使って呼び出すのは単なるSyntaxSugarと。
オブジェクトのmethodに引き続き、field。 前回はintegerだったけど、今度は自分でrectオブジェクトというのを定義してみる。
Cecil> object rect;
Cecil> var field height(@rect);
get accessor for field height(@rect)..Added
set accessor for field height(@rect)..Added
initializer accessor for field height(@rect)..Added
Cecil> var field width(@rect);
get accessor for field width(@rect)..Added
set accessor for field width(@rect)..Added
initializer accessor for field width(@rect)..Added
var field XXX(@型); でフィールドの宣言になるらしい。 で、getterはXXXという名前で、setterはset_XXXという名前になる、と。 let var と同じで、field の前に var を付けないで宣言すると setterは生成されないことになる。
Cecil> let r := rect;
Cecil> r.set_width(100);
Cecil> r.set_height(50);
Cecil> r.width * r.height;
5000
methodも宣言してみて組み合わせる。
Cecil> method area(x@rect) { x.width() * x.height(); };
Added method area(@rect) (C-interpreted)
Cecil> r.area();
5000
Cecil> r.area;
5000
Cecil> area(r);
5000
ドットを付ける記法は単なる関数へのパラメータ渡しのSyntax Sugarなので、 最後のような書き方ができるのは自然。0引数関数 … あるいはドットで最初の引数を頭に持っていった1引数関数 … の場合、 ()は付けても付けなくてもよいみたい。
Cecil> object square isa rect;
継承の書き方だけメモっておく。isa。継承とsubtypingは違うのだよ、 みたいなことも書いてあった。それは重要っぽい。
Aというオブジェクトとhelloというメソッド。
Cecil> object A;
Cecil> method hello(a@A) { print_line("i'm A"); }
Added method hello(@A) (C-interpreted)
B is-a A。Bに対するhelloメソッドは無いので、 自動的に親のmethodが呼ばれる。
Cecil> object B isa A;
Cecil> let x := A;
Cecil> let y := B;
Cecil> x.hello();
i'm A
Cecil> y.hello();
i'm A
B用のmethodも作成して、C++で言う仮想関数のオーバーライド。
Cecil> method hello(b@B) { print_line("i'm B"); }
Added method hello(@B) (C-interpreted)
Cecil> x.hello();
i'm A
Cecil> y.hello();
i'm B
…ところで今気付いたが、let x := A は必要ないようだ。
A.hello(); や B.hello(); とそのまま書くことで動く。
頭の中がJavaやC++のクラス指向なので、Aという型を宣言して
そのインスタンスxを作る、というつもりでいたのだけれど、
object A と書いた時点で A はインスタンスなのか?
でないと確かに let x:=1 と整合性が取れないし。JScriptで
var A = new Object();
と書くのと同じ感じだね。
よく見たら 2.1.2 Object Instantiation という章があって、
Cecil> let s1 := object isa square;
と書けるそうだ。なるほど、だいたいわかった。 今回までのまとめということで、長めに書いてみよう。 ついでにファイルからinterpreterへ読ませる方法など。 あと、object ... {...} による、field の初期化方法。
object shape;
method area(shape) { 0 };
object rect isa shape;
field h(@rect);
field w(@rect);
method area(x@rect) { x.h*x.w; };
object circle isa shape;
field r(@circle);
method area(x@circle) { x.r*x.r*3.14; };
let a := object isa rect {h:=100, w:=50};
let b := object isa circle {r:=10};
method hatena(i@integer)
{
if( i==1, {a}, {b} );
}
とやって
Cecil> include "test.cecil";
(略)
Cecil> hatena(1).area;
5000
Cecil> hatena(2).area;
314.000000
とまぁ、改行コードをLFにしておかないとマズいのにはまったけれど、 期待したとおりの結果。しかし、この場合 circle と rect が共通のベースオブジェクトshapeを持ってる必要は全く無いような。 shapeのareaメソッドが存在しなくても動くし。「オーバーライド」ではなく、 rectとcircleに対する「オーバーロード」の解決として、 呼び出されるmethodが決定されているようだ。型付けが静的な言語だと オーバーロードの解決はコンパイル時になるけど、動的な言語ならそれを 実行時まで遅らせることができる。
今日は時間がないのでメモ程度
object A;
object B isa A;
object C isa A;
method test(@A,@C) {print_line("AC");}
method test(@B,@A) {print_line("BA");}
method test(@B,@B) {print_line("BB");}
method test(@C,@B) {print_line("CB");}
method test(@C,@C) {print_line("CC");}
で
Cecil> B.test(B);
BB
Cecil> C.test(B);
CB
Cecil> C.test(C);
CC
となる。特にCのmethod呼び出し二つに注意。第一引数側での 動的な分岐だけでなく、引数B/Cに応じてこちらがわでも、 さらに呼び出すmethodの動的な決定が行われている。Java や C++ などの静的オーバーロード + virtual によるオブジェクトの型による実行時バインディング、 だけでなく二つ以上のオブジェクトによりディスパッチ可能。 なお、曖昧な場合は
Cecil> B.test(C);
error: message "test" with 2 args ambiguous. Could not decide between:
extension test(@A, @C)
extension test(@B, @A)
Called as:
test(B, C)
eval error: message lookup error
となる。
全てのオブジェクトの基底は any だそうだ。(x@any) { ... } で、 何でも受ける関数になる。
えらい間が空いてしまってスミマセン。11月中に終わるのかな、これ。
さてと。述語オブジェクトというもので遊んでみよう。 ML等のパターンマッチに近い機能を実現するための機構らしい。 継承とからめると更に凄いことに。
predicate positive_integer isa integer when integer > 0;
predicate non_positive_integer isa integer when not(integer > 0);
method f(n@positive_integer) { print_line("ok"); }
method f(n@non_positive_integer) { print_line("error"); }
「integerのうち、>0 という条件を満たすもの」のみを受け取る positive_integerという条件にマッチする物と、 そうでないものに分けて関数を書いている。なお、 インタプリタはpredicate構文をサポートしていないらしく、 コンパイラを使わないとVortex/Cecilではこの構文は使えない。 しかし、コンパイラの使い方がわからなかったので略。 明日にでもコンパイルできる環境を整えよう。
おそらく、methodの事前条件の表明にも使えると思う。
次は中置演算子の話。Cecilでは、中置演算子をユーザー定義することができる。
Cecil> method plus(a@integer, b@integer) { a+b; }
Added method plus(@integer, @integer) (C-interpreted)
Cecil> 1 _plus 2
3
というか、普通にmethodを定義すると、その頭に下線を付ければ中置で使えてしまう。 ちなみに逆に、組み込みの演算子も、下線を付けて普通のmethodっぽく使うことも可能。
Cecil> _+(100,200);
300
_plus の例にもどると、このままだと上の定義だけでは問題がある。
Cecil> 1 _plus 2 _plus 3
eval error: parenthesization required to mix plus with plus
(1 _plus 2) _plus 3 と解釈すべきなのか、1 _plus (2 _plus 3) と解釈すべきなのかが曖昧なわけだ。組み込みの + なら、 こういうときは左側に()が付くのと同じと決まっているので問題がないのだが。 そこでこの「結合の方向」の指定をする方法が知りたい。そこで、
precedence _plus left_associative;
とやる。ちなみにこれもインタプリタでは使えないので謎。
precedence _plus left_associative below * above +;
と書くと優先順位の指定になる。
vortex
と打って起動smallstdlib
make test.cecil
struct CecilEnvDebugInfo {
...
// void *routineAddress;
struct VoidPtr {
void* ptr;
template<typename F> VoidPtr(F p) :
ptr(reinterpret_cast<void*>(reinterpret_cast<int>(p))) {}
operator void* () const { return ptr; }
} routineAddress;
...
};
この世の終わりのような改造をほどこして突破。predicate object の例。
predicate positive_integer isa integer when integer > 0;
predicate non_positive_integer isa integer when not(integer > 0);
method f(n@positive_integer) { print_line("ok"); }
method f(n@non_positive_integer) { print_line("error"); }
f(100);
f(0);
f(-100);
実行すると、
ok
error
error
non_positive を無くしてみると
predicate positive_integer isa integer when integer > 0;
method f(n@positive_integer) { print_line("ok"); }
f(100);
f(0);
こんなん出ました。
ok
** Error: message "f" with 1 arg not understood.
Called as:
f(0)
No debugging info
子から、super classというか super objectのメソッドを呼び出す。
object window;
method what(x@window) { print( "window" ); }
object dialogbox isa window;
method what(x@dialogbox) { print( "dialogbox " ); resend; }
let w := object isa dialogbox;
w.what();
結果。
dialogbox window
多重継承のせいでresendが曖昧になるときには、
object dialogbox isa window;
method what(x@dialogbox) { print( "dialogbox " ); resend(x@window); }
のようにresend先を明示すると良いそうだ。 この辺で"dynamically typed core"周りの一通りはわかったので、 次は "Static Types" に進むこととしよう。
わかった気になったけれど、今から下に書く内容は実は 激しく間違ってるかも。とにかく、概念的なところをメモります。 継承、という関係とsubtype、 という関係は別個の物として扱うことができるようだ。
method f( a@A ) ...
はオブジェクトA、ないしはオブジェクトAを継承したもの、に関するmethodで、
method f( a:A ) ...
は型A、のsubtypeとなっているようなオブジェクトに関するmethod。
method f( a@:A ) ...
はその両方の条件を合わせたもの。
representation B inherits A;
でAを継承した実装Bの宣言。
type B subtypes A;
BはAのsubtype。
object B isa A;
BはAを継承したものであってしかもAのsubtype。 上の例ではobjectやrepresentationを使い分けたけれど、 それは本質ではなくて、isa = inherits + subtypes というのが重要だ。 で、@: = @ + : と。
subtypingとinheritanceの違いは何かというと、type(型)は、 外向けのインターフェイス(signature)によって区別されるもの、 継承は、実装の再利用を指す、らしい。(あやふや)
コンテナを、配列で実装しようが連結リストで実装しようが cons 的な構造で実装しようが、例えば append() と remove() と getat() の3つのメソッドを持っているモノなら、Container という一つの[型] として統一的に扱うことができる。あるいは、扱えてしかるべきだ。 この場合は、Containerというtypeのsignatureにあたるのがappend,remove,getat になるわけですな。
もちろん、例えばContainerという抽象クラスないしインターフェイスを用意して、 listやvectorなどはContainerを継承したオブジェクト、 と考えるのが今現在私の中では定石的な方法だけど、こういう抽象化を type という概念で捉えるようだ。
対して継承は、例えば sorted_list を実現するために list から派生して、 常にsortされた状態を保てるようにlistをラッピングするような、 そういう方法のことを言うのではないかと思う。
-- honyarara
行コメント。
(-- honyarara
aiueo
--)
範囲コメント。
(** ... **)
pragma。処理系への特別な指示とか。
全然更新できんでスマンですーm(_ _)m
forall T: template object hoge[T];
let x := concrete object isa hoge[int];
Parametarized Typeー。
また恐ろしく間が空いてしまいました。
上のように宣言した hoge[T] の method を定義するときは、
forall T: method f( x@hoge[T] ) : T {...}
ってな感じに書くとのこと。forall T が C++ の template<typename T> みたいなもんですね。これのSyntaxSugerで、
method f( x@hoge[`T] ) : T {...}
と型パラメータにbackquoteをつけて、forallを省略することも可能だそうだ。
bounded-polymorphism / 制限付きの多態。
forall T: template object my_vector[T] where T <= num;
forall T: template object map[T] where signature <=(T,T):bool;
numのsubtypeである型のみ、とか <= で比較できる型のみ、とか。
abstract object ordered[T] where T <= ordered[T];
extend num isa ordered[num];
...
F-bounded polumorphism (自分を引数にとるparametarized typeから派生すること?) も自然に書けばそのまま解釈されるとのこと。
typeの話が入ったあたりからよくわからなくなってきましたが、 一通り把握できたので、ここからはどんどん書いてみようタイム。
簡単な入出力。
-- method ask( prompt @: string ) : string
-- プロンプトを出して、入力を読みとって返す
--
-- method _|| ( s @: string, s @: string ) : string
-- method _|| ( s @: string, c @: char ) : string
-- method _|| ( c @: char, s @: string ) : string
-- 文字列連結
let name := ask( "what's your name? " );
print_line( "Hello, " || name || '!' );
what's your name? k.inaba Hello, k.inaba!
Cで言う関数ポインタをもう少し賢くしたようなのがclosureで、
closure型は &(仮引数リスト)仮返値
と表されます。
closureを実際に記述するには、0引数なら {...}
、1引数以上なら
&(仮引数リスト) { ... }
と書く。
-- 引数: 0引数を取ってstringを返すclosure
-- 返値: 引数のclosureの実行結果
--
-- eval によって closure を実行できます。
method do( f : &():string ) : string { eval(f) }
-- "hello" を返すclosureをdoに渡した結果は…
let x := do( {"hello"} );
print_line( x );
hello
要はループとは、条件判断の結果を返すclosureと、 実行部分のclosureを渡せるmethod。smalltalk的。
-- method while( cond : &():bool, c : &():void ) : void
-- closureを二つ受け取ってループ
--
-- = は意味的な一致で、== は物理的な一致を検査する。
-- "abc" = "abc" だが、"abc" == "abc" ではない。
-- おのおの否定は、 != と !== になる。
let var i := 0;
while( {i != 5},
{
print_line( i );
i := i + 1;
});
0 1 2 3 4
-- method if( @:true, :&():`T, :&():`T) :T;
-- method if( @:false, :&():`T, :&():`T) :T;
-- 条件分岐。true.if( a, b ) が a を実行して、
-- false.if( a, b ) が b を実行するようになっているので、
-- 実行時にどちらのオブジェクトのmethodが呼ばれるかで
-- 適切に分岐が行われる、ということらしい。
-- 返値は `T ということで、if の型は parameterized type となっている。
-- method loop_exit( : &( &():none ) ) : none
-- ループを作るかなり原始的なmethod。
-- 「引数を一つ受け取るclosure」を引数にとる。
-- で、その引数のclosureが呼び出されるまで、ひたすら
-- ループし続ける、といううmethodとなっている。
-- my_until 引数などの型はwhileと同じ。
method my_until( cond : &():bool, c : &():void ) : void
{
loop_exit(
&( break: &():none )
{
if( eval(cond), { eval(break) }, c )
}
);
}
-- 以下、さっきのwhileをuntil型に変更しただけ
let var i := 0;
my_until( {i = 5},
{
print_line( i );
i := i + 1;
});
0 1 2 3 4
条件 cond の評価結果が true なら、ループ終了用closureである breakを呼んで、falseなら実行部分 c を評価、をひたすら繰り返すのが until の動作になります。
-- template object m_list[T] isa list[T], extensible_sequence[T]
-- extend m_list[`T <= comparable[T]] isa removable_collection[T];
-- 変更可能な連結リスト
--
-- method add( @:m_list[`T], :T ) :void;
-- は先頭に追加するらしい
let x := concrete object isa m_list[int];
x.add( 10 );
x.add( 20 );
x.add( 30 );
x.add( 40 );
x.do( &(e){ print(e); } );
40302010
object nil;
method do( @nil, func: &(:T):void ) {}
method do_r( @nil, func: &(:T):void ) {}
object node[T];
var field next( @node[`T] );
field data( @node[`T] );
method add_tail( s@node[`T], obj:T )
{
if( s.next == nil, {
s.next := object isa node[T]{next:=nil, data:=obj};
},{
s.next.add_tail( obj );
});
}
method do( s@node[`T], func: &(:T):void )
{
eval( func, s.data );
s.next.do( func );
}
method do_r( s@node[`T], func: &(:T):void )
{
s.next.do_r( func );
eval( func, s.data );
}
object my_list[T];
private field head( @my_list[`T] ) :=
object isa node[T] { next:=nil };
method add( s@my_list[`T], obj:T )
{
s.head.next :=
object isa node[T] { next:=s.head.next, data:=obj };
}
method add_tail( s@my_list[`T], obj:T )
{
s.head.add_tail( obj );
}
method do( s@my_list[`T], func: &(:T):void )
{
s.head.next.do( func );
}
method do_r( s@my_list[`T], func: &(:T):void )
{
s.head.next.do_r( func );
}
let t := object isa my_list[string];
t.add( "1" );
t.add( "2" );
t.add( "3" );
t.add_tail( "4" );
t.do( &(x){ print_line(x) } );
t.do_r( &(x){ print_line(x) } );
…というのを書いてみた。やっぱり型/signature関係がよくわからんので、 全体的に省略。template object ... としたときとただの object ... としたときはどう違うのかも不明なので略。
list.cecil を読んでみる。注目点を抜粋しつつ。
abstract object list[T] isa sequence[T];
extend type list[`T] subtypes list[`S >= T];
list[T] は sequence[T] である、と。で、T が S の subtype な時、 list[T] は list[S] の subtype。ちなみに sequence[T] isa collection[T] で、 collection に対しては is_empty や length、do などの method が使えて、 sequence に対しては連結演算子 || が定義されていることになっている。
signature first(list[`T]):T;
signature rest(list[`T]):list[T];
で、一般的な list は、それに加えて更に first(最初の要素)/rest(残りのリスト) を取得することができるもの、となる。
abstract object simple_list[T] isa list[T],
functionally_extensible_collection[T];
var field signature first(simple_list[`T]):T;
var field signature rest(simple_list[`T]):simple_list[T];
具体的な実装その1。simple_list。Lisp の cons/car/cdr みたいな方法です。
1, 2, 3, 4, 5 というリストを
cons( 1, cons( 2, cons( 3, cons( 4, cons( 5, nil )))))
と表現する的な。なので、consペアの左側が first, 右側が rest
として自然にlist[T]のsignatureを満たすことができます。
concrete representation nil[T] isa simple_list[T];
method first(@:nil[`T]):none {
error("accessing first element of empty list") }
method rest(@:nil[`T]):none {
error("accessing rest of empty list") }
method length(@:nil[`T]):int { 0 }
method is_empty(@:nil[`T]):bool { true }
method do(@:nil[`T], :&(T):void):void {}
method reverse_do(@:nil[`T], :&(T):void):void {}
method copy(n@:nil[`T]):simple_list[T] { n }
simple_list[T] の representation は、nil[T] か、もしくは
template representation cons[T] isa simple_list[T];
var field first(@:cons[`T]):T;
var field rest(@:cons[`T]):simple_list[T] := nil[T];
method cons(hd:T, tl@:simple_list[`T]):cons[T] {
concrete object isa cons[T] { first := hd, rest := tl } }
method length(c@:cons[`T]):int { 1 + c.rest.length }
method is_empty(@:cons[`T]):bool { false }
method do(c@:cons[`T], closure:&(T):void):void {
eval(closure, c.first);
do(c.rest, closure);
}
method reverse_do(c@:cons[`T], closure:&(T):void):void {
reverse_do(c.rest, closure);
eval(closure, c.first);
}
method copy(c@:cons[`T]):simple_list[T] { cons(c.first, c.rest.copy) }
cons[T] となっている。
これを見て template object と concrete object の違いについて再度考えてみた。
「nil[T]
は常に一つだけ」だ。どういう事かというと、要するに、
object isa nil[T]
の形で派生オブジェクトを作ることがない。
対して cons[T]
は常に、cons[T] の定義を元にしたオブジェクトを
concrete object isa cons[T]
によって生成してから使う。
つまり、template というのはそういう意味では無かろうか?実験。
Cecil> concrete object co;
Cecil> field x(@co) := 100;
get accessor for field x(@co)..Added
initializer accessor for field x(@co)..Added
Cecil> co.x
100
Cecil> template object to;
Cecil> field x(@to) := 100
get accessor for field x(@to)..Added
initializer accessor for field x(@to)..Added
Cecil> to.x
eval error: referencing to, an abstract/template/predicate object
そういうことで良さそうな気がする。
もう一度、継承 と subtype の違いなるものについて再確認を試みる。
method if( @:true, ...
これは、true.if( f, g )
と書くとfが実行されて、
false.if( f, g )
と書くとgが実行されるという分岐を行うmethod。
ここでは、必ず継承関係を表す @ が必要だ。何故かというと、
trueと完全に同じインターフェイスを持っているオブジェクトだからと言って、
そのオブジェクトがtrueであるとは限らないから。例えば
falseというオブジェクトは論理演算を全てサポートするだろうから、
結果的にtrueと同じインターフェイス(signature)を持っているかもしれない。
@ を使えば、継承関係、つまり isa、trueである
オブジェクトのみを受けることができる。
method if( ..., thn: &():`T, ...
ここは : で良い。ゼロ引数closureと同じインターフェイスを持っているもの、 つまり eval メソッドを持っているものなら何を渡しても問題ないからだ。 だがここで、@:zero_ary_closure[`T] みたいにして、closure の派生オブジェクトであれば何でもよい、という形の制約にすることも、 また不可能ではないと思う。eval method を持っていることを強制するには、 基底オブジェクトでevalを持っておけば派生オブジェクトにもそれが伝わるし、 必要ならばオーバーライドして動作を変えればよいし。
次の例を見てみよう。
extend type list[`T] subtypes list[`S >= T];
TがSのsubtypeなら、list[T]はlist[S]のsubtypeである、という宣言。 これは、自然にそうなる。list[S]に対してできる操作は全てlist[T]にもできる、と。 この場合、subtype関係は自然に成り立っている。
継承関係はどうだろう? T isa S なら、list[T] isa list[S] だろうか? 確かにそうかもしれない。犬が哺乳類なら、犬の集まりは哺乳類の集まりだ。 が、やはり違うとも言える。list[T] は、list[S] なるオブジェクトとは全く関係無しに、独自に定義されているからだ。 プログラム的に共通してる部分といえば、「同じ操作ができる」 という点にすぎない。であれば、[継承]ではない、もっと別の [同じ操作ができる] という概念でくくった方が適切ではないか?
…ということではないかと今は思っているのだけど、どうだろう。 全く間違っているかもしれないけれど。
いささか不完全燃焼ですが、この辺りで一旦cecilについては中断します。 CLOSとかdylanとかSelfとか、リファレンスに頻繁に名前の出てきた、 おそらくは似た他の言語を見てみてから見直してみようかと思っています。
ではでは。
subtypeとsubclassを区別する意味、やっとわかった!気がする。
# OCamlのreference
とまめっち、tossy-2、てつさんに感謝。
結局上と言っていることは同じなのだけど、 自分メモとしてメモらせていただきます。
区別しないで subtype == subclass となってる言語、例えばC++を考えると
class Base {}
class Derived : public Base {}
のとき Derived* は Base* のsubtypeだから、Base* 型の値を期待しているコードに Derived* 型の値を渡してもちゃんとコンパイルが通る。これはOK。けれど、 これを一段階複雑にした
typedef Base* (*funcB)();
typedef Derived* (*funcD)();
を考えると、funcB 型の値を期待しているコードに funcD 型の値を渡しても問題なく動作できるはずなのに、 これはコンパイルできない。funcDはfuncBのsubclassになっていないから、 その間の暗黙の変換も行われないことになっている。これは不便だ。 同じ現象は各所であらわれる。
pair<Base*, Base*>* b;
pair<Derived*, Derived*>* d;
b = d; // error
shared_ptr<Base> b;
shared_ptr<Derived> d;
b = d; // error...にならないようにtemplateで頑張ってはあるけど不自然
pair<Derived*, Derived*> の実装は pair<Base*, Base*> の実装を全く利用せずに書かれるから、 この二つは実装継承関係には無い。(そもそも、C++ ではこの2つを継承関係に置けるようにtemplateを書くことは不可能だ)
だけれど、subclassという考えから離れて DがBのsubtypeならD f()はB g()のsubtypeで、void f(B)はvoid g(D)の subtype、としておけば、安全性を失わずにより広い変換をサポートできる。 というわけで、subclassing とは別に subtyping を考えることで、 関数型や構造型などの複雑な型を用いるときに、不自然な制約がかからないで済む。
...と。やっぱりうまく書けんなぁ。