[LSP42] KotlinとJuliaとScala
(この記事はLISP Implementation Advent Calendar 16日目のためのエントリです。)
KotlinとJuliaとScalaでLISPを作りました。
https://github.com/zick/KotlinLisp
https://github.com/zick/JuliaLisp
https://github.com/zick/ScalaLisp
動機
今年の春、訳あって42個のプログラミング言語でLISP処理系を実装することになりました。これはその27〜29個目です。
Kotlinは例によって「JVMで動く(Java以外の)言語は(それなりの確率で)簡潔で簡単なはず」という私の脳内で成り立つ法則に基いて選びました。
JuliaはWikipediaを眺めている時に見つけて簡単そうだったので選んだような記憶があります。
Scalaも「JVMで(以下略)」で選びました。
Kotlinの思い出

I・D・E!! I・D・E!!
マニュアルを読みながらKotlinの処理系をインストールしていると「次にIDEの設定をします」という説明が出てきました。数百行のプログラムを書くだけなのでIDEは使わなくていいや、と思ったものの、いくら探してもコマンドラインからKotlinを使う方法が見つかりませんでした。色々と検索してもなかなか見つからず、かなり時間を書けて頑張った末にコマンドラインからの実行方法が分かりました。
% kotlinc-jvm -src kotlinlisp.kt -jar lisp.jar -includeRuntime && java -cp lisp.jar lisp.LispPackage
IDEが使えることは素晴らしいことだとは思いますが、マニュアルには是非ともIDE以外からの使い方も載せて欲しいです。
外観
今どきっぽい感じで嫌いじゃありません。トップレベルに関数が定義できるのも良いです。
class Nil {
}
val kNil = Nil()
class Cons(a : Any, d : Any) {
var car = a
var cdr = d
}
fun nreverse(l : Any) : Any {
var lst = l
var ret : Any = kNil
while (true) {
val elm = lst
if (elm is Cons) {
val tmp = elm.cdr
elm.cdr = ret
ret = lst
lst = tmp
} else {
break
}
}
return ret
}
型をそれなりに書かないといけないのは少々面倒ですが、某言語の function 名前(引数) : 型 という非常に冗長な構文を見た後だと、fun の3文字が非常に短く見えて幸せな感じです。
エラーメッセージ
Kotlinのコンパイラのエラーメッセージは一昔前のC++コンパイラ並にくどいです。例えば上のプログラムの Cons のコンストラクタの引数の型を省略してみると、次のようなエラーが出ます。
ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (42, 13) Parameters must have type annotation ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (42, 16) Parameters must have type annotation ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (231, 26) Overload resolution ambiguity: internal final var car: [ERROR : Type annotation was missing] defined in lisp.Cons internal open var: [ERROR : ] defined in root package ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (232, 23) Overload resolution ambiguity: internal final var car: [ERROR : Type annotation was missing] defined in lisp.Cons internal open var : [ERROR : ] defined in root package ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (234, 22) Overload resolution ambiguity: internal final var cdr: [ERROR : Type annotation was missing] defined in lisp.Cons internal open var : [ERROR : ] defined in root package ERROR: /Users/zick/prog/lsp42/kotlinlisp/kotlinlisp.kt: (397, 27) Overload resolution ambiguity: internal final val data: jet.Int defined in lisp.Num internal open var : [ERROR : ] defined in root package exec() finished with COMPILATION_ERROR return code
長い。長すぎる。いくらなんでも長過ぎる。一番上の行を見れば「パラメータにタイプアノテーションをつけろ」と言ってるので何を間違えたかは明らかなんですが、なにもこんなに長いエラーメッセージを出さなくていいじゃないですか。
Juliaの思い出

外観
Juliaは変数や関数に型を書きません。私にとって大事なのはその一点です。
type Cons
car
cdr
end
function nreverse(lst)
ret = kNil
while isa(lst, Cons)
tmp = lst.cdr
lst.cdr = ret
ret = lst
lst = tmp
end
ret
end
function pairlis(lst1, lst2)
ret = kNil
while isa(lst1, Cons) && isa(lst2, Cons)
ret = Cons(Cons(lst1.car, lst2.car), ret)
lst1 = lst1.cdr
lst2 = lst2.cdr
end
nreverse(ret)
end
このところ型を書く必要がある言語がずっと続いていたので型を書かない言語の素晴らしさを改めて再発見しました。型を書く必要がないという事実を前にしたら、あまり好きでないbegin/endの構文も、冗長なキーワードfunctionも大した問題ではありません。もう最高。とにかく最高。素晴らしい。
多分良い言語です
マニュアルを流し読みしたところ、Juliaはかなりいい言語のように見えました。ただ、あまりにも気軽かけるので1時間ちょっとでLISPが完成してしまったためあれこれ試す間もなく終わってしまったのが残念です。
Scalaの思い出

Scalaは実のところ使うのを避けていたんですよ。というのも、それなりに有名でカッコよく使っている人が多い言語なので、不勉強な状態でカッコ悪いLISPを書くのも嫌じゃないですか。LISPなのに。でも名前が思い浮かぶ言語がどんどん減ってきたので仕方なく使うことに。
外観
Scalaはそれなりに型を書かなければなりません。でも、型推論のおかげでかなりの箇所で型を省略できます。
def nreverse(lst: LObj) = {
def doit(lst: LObj, ret: LObj): LObj = {
lst match {
case Cons(c) => {
val tmp = c.cdr
c.cdr = ret
doit(tmp, lst)
}
case _ => ret
}
}
doit(lst, kNil)
}
関数型言語らしく末尾再帰の形で書いています。これは趣味の問題ではなく、Scalaにはwhileというループ用の構文があるものの、いわゆるbreak相当のものがないので、ちょっと凝ったループをしようと思った場合は末尾再帰を使わないとかえって難しくなります。
case class
Scalaにはcase classというものがあり、これを使うとpretty printerが作られたり、型のパターンマッチができるようになったり、色々と便利なメソッドが自動で定義されるようです。こりゃ使わざるを得ないな、と思ったら、自動で比較関数まで作ってくれるらしく、これが内容比較なので、アドレス比較をしたい私にとっては邪魔でしかありません。そこで、case classのフィールドに直接値を入れるのではなく、フィールドにclass instanceを入れることで実質的にアドレス比較を行うようにしました。
abstract class LObj
class Nil0 {
}
class Cons0(a: LObj, d: LObj) {
var car = a
var cdr = d
}
case class Nil(obj: Nil0) extends LObj
val kNil = Nil(new Nil0)
case class Cons(obj: Cons0) extends LObj
def makeCons(a: LObj, d: LObj) = Cons(new Cons0(a, d))
このやり方が普通なのか異常なのかは知りませんが、少なくても今回に限ってはそれなりにうまくいきました。行数が増えるという問題を除いては。
再帰と相互再帰
OCamlは再帰をする関数を定義するときには rec というキーワードを付けました。Scalaはといいますと、再帰をする関数は戻り値の型を明記します。上のnreverseの内部で定義してあるdoitがそうなってますね。
さて、相互再帰の場合はStandard MLとOCamlは型推論の都合上、相互再帰をする関数を and で繋ぐ必要がありました。Scalaは通常の再帰と同様に戻り値の型を明記します。
def printObj(obj: LObj): String = {
obj match {
case Nil(_) => "nil"
case Num(n) => n.data.toString
case Sym(s) => s.data
case Error(e) => ""
case Cons(_) => printList(obj)
case Subr(_) => ""
case Expr(_) => ""
}
}
def printList(obj: LObj): String = {
def doit(obj: LObj, first: Boolean, ret: String): String = {
obj match {
case Cons(c) =>
doit(c.cdr, false, ret + (if (first) "" else " ") + printObj(c.car))
case Nil(_) => "(" + ret + ")"
case _ => "(" + ret + " . " + printObj(obj) + ")"
}
}
doit(obj, true, "")
}
複数の関数を and で繋ぐよりはこっちのほうが個人的に好きです。
小学生並みの感想
やはり簡単に書ける言語は素晴らしいと思います。