[LSP42] DylanとIokeとE
(この記事はLISP Implementation Advent Calendar 22日目のためのエントリです。)
DylanとIokeとEでLISPを作りました。
https://github.com/zick/DylanLisp
https://github.com/zick/IokeLisp
https://github.com/zick/ElangLisp
動機
今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその36〜38個目です。
DylanはLISPの派生言語と知っていたので絶対楽ができると思い、最後の方に楽をしようととっておいたものを満を持して使った感じです。
IokeはIoやSmalltalkについてWikipediaで調べている時に見つけたので使いました。
Eを選んだ理由はとよく覚えていませんが、恐らくWikipediaで見つけたのだと思います。なお「E」という名の言語は複数あるようですが、今回使ったのは「分散計算のための言語」であるEです。
Dylanの思い出
ファイルがいっぱい
マニュアルにそってhello worldを作ろうとしたら「まずこのコマンド実行しろ」という記述が出てきました。実行すると複数のファイルが生成されました。そう、私の嫌いな「hello worldを作るだけでも複数のファイルを適切な名前で適切な場所に置かないといけない言語」じゃないですか。やだー。ライブラリの依存関係とか解決してくれるのはいいですが、単一のファイルでもコンパイルして実行できるようにして欲しいです。あと、Dylanのツールはとにかく大量に出力します。コンパイラも成功失敗にかかわりなく物凄い量のメッセージを出します。あまりに出力が多すぎるためコンパイルエラーを見落とすということさえもありました。どうなんだ、これ。
外観
「DylanはかつてはLISPと同様のS式を使っていたが、途中で独自の文法を使うようになった」ということは知っていたのですが、それがどんな文法かは知りませんでした。知ってびっくり。(私があまり好きじゃない)begin/end系の文法でした。
define function pairlis(lst1, lst2) local method rec(lst1, lst2, acc) if (instance?(lst1,) & instance?(lst2, )) rec(tail(lst1), tail(lst2), pair(pair(head(lst1), head(lst2)), acc)); else reverse!(acc); end; end; rec(lst1, lst2, #()); end;
LISPみたいな言語を使えると思い込んでいた私はもうショックすぎて手元が狂いそうになりました。あと、マニュアルには end if
とか end function 関数名
などと書いてあり、絶望のあまり死にそうになったのですが、 end の後の記述は省略可能と知って無事息を吹き返しました。でもまあ、見た目はアレですが、よく見るとLISPを知っている人なら簡単にLISPに書き換えられそうな感じですね。確かにLISPの派生言語という気もします。
インチキ
今回はLISPのオブジェクトはDylanのオブジェクトをそのまま使うというインチキをしました。つまり、LISPのシンボルにはDylanのシンボルをそのまま使い、LISPのコンスにはDylanのペア、LISPのSUBRにはDylanの関数をそのまま使うといった感じです。特殊なものとして、LISPのエラーはDylanの文字列を使い、LISPのEXPRはDylanのベクタを使って「引数/本体/環境」の3つ組を表しました。あとは上記pairlisのように instance? を使って型を見るだけです。
これで超簡単、超ラクチンとか思っていたんですが、いざ出来上がったものを見てみると、なんだかイマイチな感じに。単純に汚いのは最初から予想していたので別にいいんですが、なんだかよく分からないけど納得がいかない感じになってしまいました。書き直したい気もしましたが、先を急ぐためそのまま放置することに。
Iokeの思い出
Iokeは簡潔に言うと「JVMで動くIoみたいな言語」です(*)。JVMで動く言語は当たりが多いし、Ioは既に使ったことがあるので簡単に出来る予感がしました。
(*) 実際にはCLRとかの上でも動くらしいよ、お兄ちゃん!
外観
Iokeのプログラムはこんな感じの見た目です。
nreverse = method(lst, ret = kNil while(lst kind?("Cons"), tmp = lst cdr lst cdr = ret ret = lst lst = tmp) ret)
ここでIoのnreverseを見てみましょう。
nreverse := method(lst, ret := kNil while (lst tag == "cons", tmp := lst cdr lst cdr := ret ret := lst lst := tmp) ret)
代入が =
なのか :=
なのかという違いくらいしかありません。まあ、実際には見た目以上に色々と違うのですが、そのあたりは各自勉強して下さい。あとデータ型のチェックについては後述します。
mimic
Iokeのドキュメントには「Iokeのオブジェクトは0個以上の cell と 0個以上の mimic を持つ」と書いてあります。ここで言うcellは値を入れる箱で、メソッドもcellの中に入れます。で、mimicって何だ。
Nil = Origin mimic kNil = Nil mimic Cons = Origin mimic Cons initialize = method(a, d, @car = a @cdr = d) makeCons = method(a, d, Cons mimic(a, d))
IokeはIoやJavaScriptと同じprototype-baseの言語であるという前提知識のもとに見るとmimicの意味がなんとなく分かるのではないでしょうか。そう、上記プログラムの mimic はIoでいうところの clone で、オブジェクトをコピーしているわけです。
ドキュメントの続きに目を通してみると「mimic は親オブジェクト(parent of the object)とも呼ぶ」と書いてあります。じゃあ、最初からそう書け。
更にドキュメントを読むと “mimic chain” なんて言葉も出てきます。なんでそう頑なに mimic という言葉を使うんだ。それはともかく、 mimic chain のなかに目的のオブジェクトが含まれているか判定する kind? というメソッドがあります。上記nreverseの lst kind?("Cons")
です。なぜ引数が文字列なのか意味が分かりません。Ioにある同等のメソッド hasProto の引数はオブジェクトです。こっちのほうがしっくりします。ちなみに、Ioのときはこれについて速さの議論をしましたが、もう面倒なのでIokeではkind?を使わない話なんてしません。もう疲れました。
そのほかもろもろ
IoとIokeのnreverseの違いをしっかりと見た人は気づいたかもしれませんが、Ioでは while (...)
と書いてある箇所が Iokeでは while(...)
となっています。これは私が気分でスペースを入れたり入れなかったりしたというわけではありません。Iokeではメッセージに引数を付ける場合にはスペースを開けてはいけません。スペースを開けると、続くものは次のメッセージとして扱われてしまうからです。ある意味一貫したきれいな文法とも言えるんですが、ifやwhileのあとにスペースを入れてエラーになった時には困ったのでこのことはマニュアルの分かりやすいところにデカデカと書いて欲しかったです。
Iokeのドキュメントは、文法や全般的なことを書いた”Guide” と組み込みのオブジェクト一覧が書いてある “Reference” があります。しかしどちらを探しても標準入力から一行取得する方法が見つかりませんでした。しかし、よく考えてみるとIokeはJVMで動く言語であり、Javaとも連携できます。標準入力から読み込むところだけJavaのメソッドを呼んでやればいいんです。いいアイディアだと思ったんですが、試してみたらJavaの String とIokeの Text が違う型だと怒られました。必死に色々探して両者を変換する方法を見つけて何とかなったんですが、こういうのもマニュアルの分かりやすいところにデカデカと書いて欲しかったです。
これでSmalltalk系の言語はIo、Smalltalk、Iokeで3つ目となりました(処理系のインストールに失敗したので使わなかったけどSelfという言語も少し勉強しました)。思ったのは、みんなそれぞれ独自の世界を持っており、学習コストが妙に高いということです。Smalltalkは仕方ないとして、その後に続く言語がそれぞれちょっとずつ違うというのは初心者にはなかなかつらいです。短時間で学習してLISPを実装しないといけない人の気持ちも考えて欲しいです。
Eの思い出
「D言語は使ったから次はE言語だろ」という安直な考えで選びました。このE言語は分散処理のために設計されたみたいですけど、私の目的は簡単なLISPを実装することなので、分散処理についての機能は何一つ調べてないです。ごめんなさい。あとJVMで動きます。
インストール手順長すぎ
マニュアルを読んでEの処理系をインストールしようと思ったら物凄い長い手順が書いてありました。それはもう、全部見る前にそっと閉じてしまうくらい。この手順を無視してテキトーにやったら、なんだかよく分からいけど動きました。なんだかよく分かりません。
外観
EはC-likeな文法でなおかつ変数に型を書かなくていいので、なかなか私好みです。
def nreverse(var lst) { var ret := kNil while (lst.tag() == "cons") { def tmp := lst.cdr() lst.setCdr(ret) ret := lst lst := tmp } return ret }
うん。見るからに簡単そうですね。
var
上記プログラムを見ると var ret := kNil
のように var で変数を作っている箇所と、 def tmp := lst.cdr()
のように def で変数を作っている箇所があります。この違いは、 var で作った変数はmutableで、 def で作った変数はimmutableになることです。便利なんですが、気になるのは引数の nreverse(var lst)
というところ。なんだか参照渡しみたいじゃないですか。参照渡しに見えない人は見えるようになるまでFORTRAN系の言語でLISPを実装し続けてください。
:= と ==
Eの代入は :=
で、比較は ==
という演算子を使います。Eには =
という演算子はありません。これはなかなかおもしろい試みだと思いました。かならず2文字タイプしないといけないというのは手間ですが、代入と比較を間違えることはほぼなくなります。あまり好みではないですがこういうものありだと思います。
Javaとの連携
EもJVMで動く言語なのでJavaのメソッドを呼び出せます。しかし、これを試そうとすると失敗。処理系のインストールを手順通りにしなかったバチが当たったのかもしれません。しかし、IokeのときのようにJavaのメソッドを呼ばないと解決できないような問題はなかったので、すべてをEのなかで完結させることで特に問題なくLISPを実装できました。
独自の名前
残念ながらEのドキュメントはあまり充実していません。でもJVMで動く言語だし、メソッド名などはJavaと同じものを使っているのだろうと思ったら、実際はぜんぜん違う名前を使っていました。文字列の長さは length ではなく size 。部分文字列は substring ではなく、変数に対してカッコを付けて foo(start, end)
のように書きます。他にも色々と違いがあったかと思います。独自の名前をつけるのはかまいませんが、それならマニュアルを充実させて欲しいです。
小学生並みの感想
マニュアルが充実している言語はそれだけで素晴らしいと思いました。
[…] 語ゆえ、データを色々な方法で表現することが出来ます。クラスを使ってもいいし、DylanのときのようにOzの型をそのまま使うというのもひとつの手です。でも、今回のように簡単なLISPを […]