[LSP42] FantomとCeylon
(この記事はLISP Implementation Advent Calendar 12日目のためのエントリです。)
FantomとCeylonでLISPを作りました。
https://github.com/zick/FantomLisp
https://github.com/zick/CeylonLisp
動機
今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその18〜19個目です。
Fantomという言語を選んだ理由はよく覚えていませんが、Wikipediaを眺めて見つけ、簡単そうなので選んだという感じだったかと思います。
Ceylonを選んだ理由もよく覚えていませんが、「JVMで動く(Java以外の)言語は簡潔で簡単なはず」という私の脳内で成り立つ法則に基づいて、JVMで動く言語を探して選んだような気がします。
Fantomの思い出
Fantomはなんといいいますか、フツーのイマドキの言語だったような気がします。正直なところ、非常に印象が薄いです。42個の言語のなかでも印象の薄さはトップクラスかと思います。良い見方をするなら不満に思う点がほとんどなかったのだと思います。
オブジェクト指向
Fantomはオブジェクト指向言語です。オブジェクト指向言語のなかでも「俺はオブジェクト指向言語だ! プログラムを書きたかったらまずクラスを作れ! ヒャッハー!!!」という私の好みじゃないやつです。これまで使った言語の中で言語の機能としてクラスを作ることができる言語はPython、Ruby、R、Factor、CoffeeScript、TypeScript、Dart、Falcon、Groovy、JSXと非常にたくさんありますが、このなかでLISPの1つの型と実装側の1つのクラスを対応させたのはRubyとDartとJSXの3つのみです。このなかでもJSXは1つの大きいクラスを作ってサブクラスはちょっといじるだけというインチキな手法を使いました。基本的に私はクラスというものがそれほど好きではなく、小さなLISPを作るだけならクラスを使わないほうが楽に作れると考えているくらいです。しかし、Fantomはクラスを作ることを強制される(トップレベルに関数を定義できない)言語なのでクラスを作らざるを得ません。
class LObj { Str tag Int? num Str? str LObj? car LObj? cdr LObj? args LObj? body LObj? env |LObj->LObj|? fn new makeNil() { this.tag = "nil" this.str = "nil" } new makeError(Str s) { this.tag = "error" this.str = s } new makeNum(Int n) { this.tag = "num" this.num = n } new makeSym(Str s) { this.tag = "sym" this.str = s } new makeCons(LObj a, LObj d) { this.tag = "cons" this.car = a this.cdr = d } ... }
お察しの通り、1つしかクラスを作っていません。注目すべきは makeNil から makeCons までの部分です。一見メソッドの定義のように見えますが、すべて new というキーワードが付いています。実は、これらは「名前付きコンストラクタ」です。Fantomはコンストラクタに名前をつけることができるため、 makeError と makeSym のように同じ引数のコンストラクタも名前で区別することができるわけです。そう、今回はこの名前付きコンストラクタの機能を堪能するために1つしか(*)クラスを作りませんでした。名前付きコンストラクタの機能を堪能するために仕方なく1つしか作らなかったわけで、決して面倒だったわけではありませんよ。
(*) LISPのオブジェクトのために、という意味で他の目的はいくつかクラスを作ったよ、お兄ちゃん!
簡潔さ
Fantomはイマドキの言語故に、多くのものを簡潔に記述できます。文末にセミコロンなどのデリミタを付ける必要はありませんし、関数の型という複雑なものも |ArgType->RetType|
のように簡潔に書けます。クラスのメンバにアクセスするときも this は省略可能ですし、右辺から方が分かる場合は ret := kNil
のように型をつけなくても変数を作ることが出来ます。関数の戻り値の型は書く必要がありますが、無駄にコロンを書いたり、ましてや function などの長いキーワードを書く必要はないのでそれほど負担は感じません。やはり簡潔に記述できる言語は素晴らしいです。
Numeric tower?
Fantomには Int と Float というクラスがあって、どちらも Num というクラスを継承している。私はLISP感覚で「それじゃあ、Numを使っておけば整数も浮動小数点数も適切に扱えるな」と思っていたんですが、なんとびっくり。Numに対しては四則演算が出来ませんでした。何のためのNum型だよ。Fantom作った奴は何考えてるんだ、と憤りましたが、後で調べたところ、Javaも同じ仕様みたいです。流行っているんでしょうか。私は納得いきません。
Ceylonの思い出
Fantomとは逆に非常に印象深いのがこのCeylonです。Wikipediaをチラッと見た時から強烈な印象でした。
業界のアナリストからは同プロジェクトはJavaを抹殺するためのものだと言われている
これを読んだときは何言ってるんだこいつ、と思ったんですが、LISPを作り終えることには、「ああ、『業界のアナリスト』らしい発想だなぁ」と思いました。
source/lisp/lisp.ceylon
GitHubのリポジトリを見れば分かるのですが、ファイルの概念のないScratchを除く、これまでの17個の言語はすべてリポジトリのトップレベルに単一のソースファイルをおいてきました。どれも400行前後のプログラムですから単一のファイルの方が分かりやすいでしょう。しかし、Ceylonはソースファイルを source というディレクトリの中に置かなければいけません。さらにその下にモジュール名に対応したディレクトリを作らなければいけません。Ceylonでプログラムを作るには最低でも1つはモジュールを作る必要があるので、要するに、最低でも2つはディレクトリを作らなければいけないということです。この時点で私はやる気を大幅に失ってしまいました。
一応言っておきますが、私はソースファイルを階層的に管理することを否定しているわけではありません。大きなモノをつくり上げるうえではどのように階層化するか、どうやってルールを守らせるかといったことが非常に大事になるのは分かります。でもそれを言語が強要するというのが嫌いなんです。helloworldのような数行で済むプログラムを作るのにもファイルの置き場を強要するような仕様が非常に嫌いです。
同名
Ceylonは多くのライブラリクラスを提供します。これ自体は非常に素晴らしいことですが、異なるパッケージに同名のクラスがあったり、パッケージと同名のフィールドがあったりします。例えば、 ceylon.file.Reader と ceylon.io.Reader 他にも ceylon.process と ceylon.language.process など。ドキュメントがこれらの名前を省略せずに書いてあれば困ることもありませんが、Ceylonのドキュメントは平気で Reader や process などという表記をしていて、どちらか区別できません。コード片を見て真似をしても動かず。クラスを調べたら使いたいメソッドがない。色々と調べた末に同名のクラスを見つける、という酷いハマりどころを用意しています。嫌がらせ以外のなにものでもないと思います。
Nullable
Ceylonは null を許す型(nullable)と許さない型(non-nullable)を区別します。これ自体は近代的で非常に素晴らしいことだと思います。が、 process.readLine を見て考えを改めました。この標準入力から1行取得する関数の型は String で、つまり null を許さないのですが、EOFを読み込むと null を返します。非常に基本的なライブラリの関数が基本的なルールを守っていないのです。あんまりだ。幸いなことに、 non-nullable な型を nullable な型に変換することはできるので、 wrapper をかませることで難を逃れました。
String? readLine() => process.readLine(); ... // HACK: The return type of process.readLine is String but it can return // null when it reads EOF. I verified this code works on ceylon 1.0.0. if (exists line = readLine()) { ... }
numeric tower? 再び
Ceylonも Number というクラスがあるんですが、これも四則演算を実装していません。Fantomと同じです。まあ、JVMの上で動く言語なのでJavaと同じ仕様にするのは非常に自然なのでこれは仕方ないでしょう。
template classとunion type
Ceylonは非常に嫌な思い出が多いのですが、唯一楽しかったのがtemplate classとunion typeです。これについてはごちゃごちゃ書くよりコードを見たほうが早いと思うのでコードを載せます。
class ConsT<T>(T a, T d) { shared variable T car = a; shared variable T cdr = d; } alias SubrT<T> => Callable<T, [T]>; alias ExprT<T> => [T, T, T]; alias DataT<T> => Integer|String|ConsT<T>|SubrT<T>|ExprT<T>; class LObj(String t, DataT<LObj> d) { shared String tag = t; shared DataT<LObj> data = d; } alias Cons => ConsT<LObj>; alias Subr => SubrT<LObj>; alias Expr => ExprT<LObj>; alias Data => DataT<LObj>;
ConsやSubrはLObjに依存するけど、LObjはConsやSubrに依存する。このような相互再帰的な型をtemplate classを使うことで表現できるわけです。さらにunion typeを使うことでLObjが持ちうるデータを明確に定義。この部分だけは書いてて面白かった記憶があります。 data の型は is というキーワードを使って確認できます。
LObj nreverse(variable LObj lst) { variable LObj ret = kNil; while (is Cons cons = lst.data) { LObj tmp = cons.cdr; cons.cdr = ret; ret = lst; lst = tmp; } return ret; }
そのため、 tag は必要ないはずなんですが、なんで私が tag を付けたかは忘れてしまいました。恐らく消せるとは思うんですが、直そうという意欲はあまり湧きません。
※これは実装者の感覚によるもので速度を保証するものではありません
私がCeylonでLISPを書いた時に記したメモがあるんですが、非常に興味深い記述がありました。
型に関してはそれなりに楽しかったと思って実行してみたら想像を絶するくらい遅い。ヘタしたらScratchより遅い。
小学生並みの感想
不満がない言語より不満がある言語の方が印象に残るものなんだなぁ。
[…] 最下位はEの33.311秒、そしてCeylonの18.358秒、Scratchの6.25秒が続きます。 […]