とっても優しい Self の書き方

実際に書かないと分からないので色々やってみました。統語規則は大体のところ Smalltalk っぽいので(多分統語だけじゃなくって world のあり方とかその他の大部分も相当似てるような感じです、なんというかプロトタイプベース版 Smalltalk みたいな?)、先日勉強した成果がこんなところに活かされました!やったね!

なおリファレンスマニュアルがとっても読み易いのでお得な感じです。以下からダウンロードできます。本記事はリファレンスマニュアルを参考にちまちまとコードを試しながら書きました。

http://research.sun.com/self/language.html

ところで、Self は SELF って大文字で書くのと Self と先頭大文字にするの、どっちが正しいのでしょうか。私はもう後に退けないから Self って書いていくことにします。

オブジェクトの作成

オブジェクトはスロットとコードから構成されます。コードを持たないオブジェクトを data object と呼びます。

"スロットをひとつも持たない空の data object"
()

"() と同じ"
(||)

"スロット a を持つ data object、ただしスロット a の中身は nil"
(|a|)

"スロット a の中身は 1、ただしスロット a を他に変えることは出来ません"
"スロット a は read-only slot となります"
(|a = 1|)

"スロット a の中身は 1、スロット a に対する割り当てが可能"
"スロット a は read/write slot となります"
(|a <- 1|)

"複数のスロットを設定する時には . で区切ること"
"この場合には a, b, c の三つのスロットを持つオブジェクトが作成されます"
(|a. b. c|)

空の data object である () はスロットに本当に何も持たないので、() = () のように比較すら出来なくてびっくりします(ただし _Eq: という primitive が存在するので () _Eq: () で一応比較出来なくはありません。また本来は default behavior というものであらかた振舞いが得られるようになっているようなのですが、() に関してはそれも利かないようです)。

スロットの内容を取り出したい時には、そのスロット名をメッセージとして送ります。

(|a = 'abc'|) a
"=> abc"

read/write slot は例えば (|a <- 1|) の時、a: という名前のスロットが作成されて、その内容が割り当てメソッドになります。Ruby でいうところの attr_accessor のことですよね。記法が簡単でとってもお得だと思います。

"a に 10 を割り当て"
(|a <- 1|) a: 10

メソッドの作成

では次にメソッドを作成してみましょう。メソッドはコードを持つオブジェクトで、parent と arguments という名前のスロットを持ちます。

"a = ('a' printLine) の部分がメソッドの作成、これもオブジェクト"
"最後の a で作成したメソッドを呼び出しています"
(| a = ('a' printLine) |) a
"=> a"

"ちなみに以下はなぜかダメ、良さそうなものなのに"
(| a = (|| 'a' printLine) |)
No '||' slot found in a slots object.

なお、メソッドはメソッドの中では作成できません。そういう目的にはブロックを使えということでしょうか。

(| a = (|a| (|b| 'a' printLine)) |)
inner methods are no longer supported, slot-list within a sub expression is not legal.

次に引数を持つ複数持つメソッド(keyword methods)を作成します。それぞれの引数には対応するキーワードを設定しますが、第一キーワードは最初の文字が小文字もしくは _ になります(ただし _ から始まる場合は primitive になるそうですので、通常は小文字で始めておけばいいと思います)。下の例では a: の部分が第一キーワードになります。第二キーワードは大文字から始めなければなりません。下の例では B: の部分が第二キーワードになります。引数は :a や :b のように前にコロンを付けてやるだけです。メソッドを呼ぶ時にはキーワード付きで呼び出します。

(| a: B: = (|:a. :b| a + b) |) a: 1 B: 2
"=> 3"

他にも unary methods やら binary methods やらありますが、特に難しいことや紛らわしいこともないのでリファレンスマニュアルをご覧下さい。

最後におまけのミニ情報ですが、メソッドの再割り当ては実装されてないそうです。エラーメッセージでごめんって言ってるってことは言語仕様的には本来認められるってことでしょうかね。

(| a <- ('a' printLine) |) a
"=> sorry - assignable methods not implemented."

ブロック

ではブロックを見てみましょう。ブロックはスロット value に不可視のブロックメソッドを持つオブジェクトだそうです。ブロックメソッドはあるけど見えないんだって。ブロックは次のようにして作ります。

[|:a| a * a]

これはメソッドオブジェクトとほとんど同様ですが、暗黙的レシーバ self がブロック作成時の self になる点で異なります。つまり早い話が Smalltalk とか Ruby のブロックと一緒の働きですよね。

ブロックを呼び出す時には次のようにします。

[1] value
"=> 1"

[|:a| a * a] value: 2
"=> 4"

[|:a. :b| a * b] value:2 With:3
"=> 6"

なんか With: をたくさん重ねちゃえば引数をいくつも渡せるみたいです(こういうメソッドの定義方法が分からないのですが、これって block value だけの特別扱い?)。With: も第二キーワード以降になるから、メソッド呼び出しの時と同様に大文字で始まっています。

delegation による継承

さて肝心の delegation による継承システムを見てみましょう。delegation とは child が parent の振舞いを借りて self を child として実行することです。

delegation って何? - ¬¬日常日記

Self においては parent となるオブジェクトをスロット(parent slot)で指しますが、この時末尾に * を付けます。

"parent* に割り当てられた traits pair が parent オブジェクト"
(|parent* = traits pair|)

あくまでも delegation であって consultation ではないので次のようものはうまく行きません。

(|parent* = 'abc'|) size
"=> badTypeError: the '_ByteSize' primitive failed."
"   Its receiver was a slots object."

ちなみに parent slot は必ずしも read-only slot である必要があるわけではありません。よって次のように parent slot の再割り当てを行なうことも可能です。

(|parent* <- traits pair|) parent: traits point

parent slot は末尾に * を付けることで作成します。従ってスロット名が parent である必要もなければ(ただし慣習的には parent* とするようです)、一つのオブジェクトに複数の parent slot を作成することも出来ます。このことから分かるように Self では実質的に多重継承が可能です。

"a* に割り当てられた trais pair と
"b* に割り当てられた traits point の両方が parent"
"ちなみに _AddSlots で shell にスロット obj を割り当ててます"
_AddSlots: (| obj = (|a* = traits pair. b* = traits point. x. y|) |)

obj x: 10
obj y: 2

"セレクタ aspectRatio は traits pair のスロットにマッチします"
obj aspectRatio
"=> 0.2"

"セレクタ asRectangle は traits point のスロットにマッチします"
obj asRectangle
"=> a rectangle(0@0 # 10@2)"

しかし多重継承をやっちゃうとセレクタが複数のスロットにマッチしてしまうという問題点があります。この時どうもエラーになるようです。どちらのスロットを優先するかといった制御が可能なのかどうかはまだ良く分かっていません。

"copyX:Y:は traits pair と traits point の両方に存在します"
obj copyX: 1 Y: 2
"=> More than one 'copyX:Y:' slot was found in a slots object."
"   The matching slots are: traits point and traits pair."

よって実際のところはこうした多重継承を用いるのではなく、Ruby 同様により安全な Mixin を使用するのが良いようです。とは言えば mixin もただのオブジェクトで、parent slot として複数設定しても困らないように parentless で安全に設計されているだけのようです。

Self で循環継承

じゃあいつもの通り循環継承にチャレンジしてみたいと思います。

"最初に parent が空のスロットを作成します"
_AddSlots: (| obj = (|parent*|) |)

"次に parent を自分自身に設定します"
obj parent: obj

"ではいざメソッド探索に出発!"
obj x
"=> No 'x' slot found in a slots object."

循環継承できちゃいました!しかも未知のメソッドを探索してもきちんと停止します。実は Self は循環継承を言語仕様において明示的に許可している珍しい言語なのでした。リファレンスマニュアルから該当個所を引用します。

2.3.8 The lookup algorithm
The lookup algorithm recursively traverses the inheritance graph, which can be an arbitrary graph (including cyclic graphs). No object is searched twice along any single path.

つまりメソッド探索において一度探索した parent は二度目以降は探索しないことで循環継承に対応しています。私はこれが一番正しい循環継承の扱い方だと思っています。

というわけだから、Javascript って循環継承の扱いだけでなく多重継承も Mixin もないわけで(実質的に同じことは出来るのだろうとは思いますので実際的な不満があるわけではないのですが)色々と骨抜きだよなぁ、と思いました。ひょっとして Javascript はプロトタイプベースとは言え Self の影響をあまり受けなかったのでしょうか?

__proto__ で循環継承 - ¬¬日常日記

まとめ

大体のところこんな感じです。とっても過激にオブジェクトしてて美しいですね!統語は簡潔だし、意味論も理解し易いので是非一度リファレンスマニュアルをご覧下さい。プロトタイプベースの言語と言えば javascript と思っていましたが、その礎たる Self はもっと優れた言語であるように思いました。まさかこんなに良く出来てるとは予想もしませんでした、すごいよ Self さん!

とは言え、これまで全然触れなかったことからも分かるように、ローカル変数とかそういうのが全然ないわけですよ。ローカル変数相当が欲しけりゃブロックでなんとしなさい!というのは分かるのですが、半端じゃなく面倒です。またオブジェクトにスロットを簡単に追加する糖衣構文もなければ inner method も禁止されているので色々面倒だと思います。面倒なのは我慢するとしても、objects/core 以下に存在するソースを読めば分かるように実際的なプログラムにおける可読性が極めて悪いのは簡単に予想できます。マクロが好きじゃない私でもこれはさすがにマクロ欲しくなりました。

美しさの反面、これじゃ流行らないわけだよ!と思いました。やっぱりこう、うまくいかないものですね。しかしそこで iolanguage ですよ、と話は続く、ような気がします。

あとこれだけなんでもかんでもオブジェクトでなんでもかんでもメッセージ送りまくってりゃ、そりゃ VM で頑張るしかないよな、とも思いました。目的のためには手段を選ばずで VM の高速化手法を開発していったという歴史のようですから、なんというか頭の良い人達の無茶苦茶な頑張りっぷりには頭が下がります。こっそり GC も世代別 GC だったりするらしいし、言語設計の美しさと実装の剛腕さの両立は大変だったんだろうなぁと苦労がしのばれます。

とにかく面白い言語なので皆さんも是非 Self やってみて下さい。環境によっては動かすのが大変かも知れませんけどね。

Self 処理系を色々試してみました - ¬¬日常日記