A micro-manual for LISP Implemented in Groovy

元ネタは Nurullah Akkaya 氏の A micro-manual for LISP Implemented in C
その元ネタは Lisp の始祖である John McCarthy 氏の A micro-manual for LISP - not the whole truth*1
LISP による実装は 50 行ぐらい、C による実装は 380行ぐらいある。
この差は言語によるもの以外に LISP の実装は EVAL 関数だけを定義しているが C の実装は入出力まで実装しているため。
他の言語でも実装されていて Scala では Suzuki Hisao 氏の http://www.oki-osk.jp/esc/scala-lisp/ を見つけた。


本家の LISP 版のように EVAL のみ実装し、以下のテストが通るところを目指す*2

import static Lisp.*

lisp { shell ->
  assert A         == shell << [QUOTE, A]
  assert [A, B, C] == shell << [QUOTE, [A, B, C]]
  assert A         == shell << [CAR, [QUOTE, [A, B, C]]]
  assert [B, C]    == shell << [CDR, [QUOTE, [A, B, C]]]
  assert [A, B, C] == shell << [CONS, [QUOTE, A], [QUOTE, [B, C]]]
  assert true      == shell << [EQUAL, [CAR, [QUOTE, [A, B]]], [QUOTE, A]]
  assert []        == shell << [EQUAL, [CDR, [CDR, [QUOTE, [A, B]]]], [QUOTE, A]]
  assert true      == shell << [ATOM, [QUOTE, A]]
  assert B         == shell << [COND, [[ATOM, [QUOTE, A]], [QUOTE, B]], [[QUOTE, T], [QUOTE, C]]]
  assert [A, D]    == shell << [[LAMBDA, [X, Y], [CONS, [CAR, X], Y]], [QUOTE, [A, B]], [CDR, [QUOTE, [C, D]]]]
  assert true      == shell << [LABEL, FF, [LAMBDA, [X, Y], [CONS, [CAR, X], Y]]]
  assert [A, D]    == shell << [FF, [QUOTE, [A, B]], [CDR, [QUOTE, [C, D]]]]
  assert true      == shell << [LABEL, XX, [QUOTE, [A, B]]]
  assert A         == shell << [CAR, XX]
}


REPL を実装しないのは Groovy で引用符なしで実現する方法を思いついたため。
A とか QUOTE など存在しないものはプロパティではと判定されることを利用してみた。
XXXBuilder は同じように invokeMethod を利用しているがそれのプロパティバージョン。

groovy:000> A
ERROR groovy.lang.MissingPropertyException:
No such property: A for class: groovysh_evaluate
        at groovysh_evaluate.run (groovysh_evaluate:2)
        ...


ドキュメントの最初に S式の説明がある。

  • LISPデータとは、S式のことでアトムでありリストである
  • アトムとは、文字列と数字とLISPで使用されていない文字である
  • リストの構成は、 '(' の後に 0 個以上の空白で区切られたアトムやリストが続き ')' で終わる

空でないリストがリストでそれ以外はアトムである ということらしい。
この辺りは Scheme *3 と違っている*4のでややこしいのだが、Groovy も Java も知らない人にとっては == は同値比較なのか同一比較なのか混乱するだろうと思えばお互い様かもしれない。


ちなみに Groovy は昔は === で同一比較を行っていたらしい*5
試してみると is 使えといわれた。Groovy はたまに例外でアドバイスしてくれるけどこのあたりはどうなっているのだろう*6

groovy:000> 1 === 1
ERROR org.codehaus.groovy.control.MultipleCompilationErrorsException:
startup failed:
groovysh_evaluate: 2: Operators === and !== are not supported. Use this.is(that) instead @ line 2, column 1.
   1 === 1
   ^


以下、実装

class Lisp {
  static shell() {
    new Shell()
  }
  static def lisp(Closure block) {
    lisp(shell(), block)
  }
  static def lisp(Shell shell, Closure block) {
    block.resolveStrategy = Closure.DELEGATE_ONLY
    block.delegate = new GroovyObjectSupport() {
      def getProperty(String name) { name }
    }
    block(shell)
  }
  static class Shell {
    def env = [:].withDefault { it }
    def leftShift(sexp) {
      println sexp
      eval(sexp, env)
    }
    def isList(sexp) {
      sexp in List && sexp != []
    }
    def isAtom(sexp) {
      !isList(sexp)
    }
    def eval(sexp, Map env) {
      if (isAtom(sexp)) {
        env[sexp]
      } else if (isAtom(sexp.head())) {
        def (name, arg1, arg2) = sexp
        if (name == "QUOTE")
          arg1
        else if (name == "CAR")
          eval(arg1, env).head()
        else if (name == "CDR")
          eval(arg1, env).tail()
        else if (name == "CONS")
          [eval(arg1, env), *eval(arg2, env)]
        else if (name == "EQUAL")
          eval(arg1, env) == eval(arg2, env) ?: []
        else if (name == "ATOM")
          eval(arg1, env).with { it == [] || !(it in List) } ?: []
        else if (name == "COND")
          sexp.tail().find { eval(it.head() ,env) == true }.with { eval(it.tail().head(), env) }
        else if (name == "LABEL") {
          if (arg2.head() == "LAMBDA")
            env[arg1] = arg2
          else
            env[arg1] = eval(arg2, env)
          true
        } else {
          def value = [env[name], *sexp.tail()]
          if (isList(value) && isList(value.head()) && value.head().head() == "LAMBDA")
            eval(value, env)
          else
            value.collect { eval(it, env) }
        }
      } else if (sexp.head().head() == "LAMBDA") {
        def (_, params, body) = sexp.head()
        def evlis = sexp.tail().collect { eval(it, env) }
        eval(body, env + [params, evlis].transpose()*.asType(MapEntry))
      }
    }
  }
}

env を Map に変えたり、LABEL のあたりが本家と違う気がするが大体同じだと思う。
getProperty で捕捉できるのはプロパティと判断されるものだけなので例えば以下のものは捕捉できない。

*1:リンク先の GoogleDocs で読める

*2:ドキュメントと同じ内容だと思うが C のテストケースを参考にした

*3:Scheme 手習い』を読んでいる

*4: () は list? で #t だった

*5:角谷信太郎氏の Groovy - Java用スクリプト言語 で確認

*6:1.8 では GroovyBugError が発生するので 1.7 で試した