[LSP42] KotlinとJuliaとScala

(この記事はLISP Implementation Advent Calendar 16日目のためのエントリです。)

KotlinJuliaScalaで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 で繋ぐよりはこっちのほうが個人的に好きです。

小学生並みの感想

やはり簡単に書ける言語は素晴らしいと思います。

Leave a Reply