簡易LISP処理系の実装例（Julia版）

【他言語版へのリンク記事】[簡易LISP処理系の実装例【各言語版まとめ】](https://qiita.com/ytaki0801/items/dcb0fe48cbeab30ac40d)

この記事は，下記拙作記事のJulia版を抜粋・修正したものを利用した，簡易LISP処理系（["McCarthy's Original Lisp"](http://paulgraham.com/lispcode.html)）の実装例をまとめたものです．

* ~~[『括弧文字列』簡易パーサ実装例まとめ](https://qiita.com/ytaki0801/items/b8d7d4c778fe98966ce5)~~
（Julia版はS式入出力を先行作成しました）
* [リスト処理関数（cons,car,cdr,eq,atom）実装例まとめ](https://qiita.com/ytaki0801/items/a11c8143c5c63a406c7f)

最低限の機能をもったLISP処理系の実装の場合，本体である評価器（eval）実装はとても簡単であり，むしろ，字句・構文解析を行うS式入出力やリスト処理実装の方が開発言語ごとの手間が多く，それが敷居になっている人向けにまとめています．

# 処理系の概要
実行例は次の通り．Julia 1.0.3にて確認．

```Julia
$ julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.0.3
 _/ |\__'_|_|_|\__'_|  |  Raspbian ⛬  julia/1.0.3+dfsg-4+rpi1
|__/                   |

julia> include("./jmclisp.jl")
s_readlines (generic function with 1 method)

julia> s_rep(s_readlines())
(car (cdr '(10 20 30)))

"20"

julia> s_rep(s_readlines())
((lambda (x) (car (cdr x))) '(abc def ghi))

"def"

julia> s_rep(s_readlines())
((lambda (f x y) (f x (f y '()))) 'cons '10 '20)

"(10 20)"

julia> s_rep(s_readlines())
((lambda (f x y) (f x (f y '())))
 '(lambda (x y) (cons x (cons y '())))
 '10 '20)

"(10 (20 ()))"

julia> s_rep(s_readlines())
((lambda (assoc k v) (assoc k v))
 '(lambda (k v)
    (cond ((eq v '()) nil)
          ((eq (car (car v)) k)
           (car v))
          ('t (assoc k (cdr v)))))
 'Orange
 '((Apple . 120) (Orange . 210) (Lemon . 180)))

"(Orange . 210)"
```

実装内容は次の通り．

* ["McCarthy's Original Lisp"](http://paulgraham.com/lispcode.html)をベースにした評価器
* 数字を含むアトムは全てシンボルとし，変数の値とする場合は`quote`（`'`）を使用
* 構文として`quote`の他，`cond`と`lambda`が使用可能
* 組込関数：`atom` `eq` `cons` `car` `cdr`（内部でコンスセルを作成）
* 真偽値は`t`（真）および`nil`（偽）＝空リスト＝`nothing`
* エラーチェックなし，モジュール化なし，ガーベジコレクションなし

"McCarthy's Original Lisp"の詳細については[まとめ記事](https://qiita.com/ytaki0801/items/dcb0fe48cbeab30ac40d)を参照．ダイナミックスコープということもあり，実行例ではlambda式を`letrec`（Scheme）や`labels`（Common Lisp）などの代わりに使用しています．

#実装例

##ソースコード一式

```jmclisp.jl
#
# JMC Lisp: defined in McCarthy's 1960 paper,
# with S-expression input/output and basic list processing
#


# basic list processing: cons, car, cdr, eq, atom

cons(x, y) = (x, y)
car(s) = s[1]
cdr(s) = s[2]
eq(s1, s2) = s1 == s2
atom(s) =
  isa(s, String) || isa(s, SubString) || isa(s, Nothing) || isa(s, Bool)


# S-expression input: s_read

function s_lex(s)
  for p = "()'"
    s = replace(s, p => " " * p * " ")
  end
  return split(s)
end

function s_syn(s)
  t = pop!(s)
  if t == ")"
    r = nothing
    while s[end] != "("
      if s[end] == "."
        pop!(s)
        r = cons(s_syn(s), car(r))
      else
        r = cons(s_syn(s), r)
      end
    end
    pop!(s)
    if length(s) != 0 && s[end] == "'"
      pop!(s)
      return cons("quote", cons(r, nothing))
    else
      return r
    end
  else
    if length(s) != 0 && s[end] == "'"
      pop!(s)
      return cons("quote", cons(t, nothing))
    else
      return t
    end
  end
end

s_read(s) = s_syn(s_lex(s))


# S-expression output: s_string

function s_strcons(s)
  sa_r = s_string(car(s))
  sd = cdr(s)
  eq(sd, nothing) && return sa_r
  atom(sd)        && return sa_r * " . " * sd
                     return sa_r *  " "  * s_strcons(sd)
end

function s_string(s)
  eq(s, nothing) && return "()"
  eq(s, true)    && return "t"
  eq(s, false)   && return "nil"
  atom(s)        && return s
                    return "(" * s_strcons(s) * ")"
end


# JMC Lisp evaluator: s_eval

caar(x) = car(car(x))
cadr(x) = car(cdr(x))
cadar(x) = car(cdr(car(x)))
caddr(x) = car(cdr(cdr(x)))
caddar(x) = car(cdr(cdr(car(x))))

s_null(x) = eq(x, nothing)

s_append(x, y) =
  s_null(x) ? y : cons(car(x), s_append(cdr(x), y))

s_list(x, y) = cons(x, cons(y, nothing))

s_pair(x, y) =
  s_null(x) && s_null(y) ? nothing :
  !atom(x)  && !atom(y)  ?
    cons(s_list(car(x), car(y)),
         s_pair(cdr(x), cdr(y))) :
  nil

s_assoc(x, y) =
  eq(caar(y), x) ? cadar(y) : s_assoc(x, cdr(y))

function s_eval(e, a)
    if eq(e, "t")   return true
    elseif eq(e, "nil") return false
    elseif atom(e)      return s_assoc(e, a)
    elseif atom(car(e))
        eq(car(e), "quote") && return cadr(e)
        eq(car(e), "atom")  && return atom(s_eval(cadr(e), a))
        eq(car(e), "eq")    && return eq(  s_eval(cadr(e), a),
                                           s_eval(caddr(e), a))
        eq(car(e), "car")   && return car( s_eval(cadr(e), a))
        eq(car(e), "cdr")   && return cdr( s_eval(cadr(e), a))
        eq(car(e), "cons")  && return cons(s_eval(cadr(e), a),
                                           s_eval(caddr(e), a))
        eq(car(e), "cond")  && return evcon(cdr(e), a)
        return s_eval(cons(s_assoc(car(e), a), cdr(e)), a)
    elseif eq(caar(e), "lambda")
        return s_eval(caddar(e),
                      s_append(s_pair(cadar(e), evlis(cdr(e), a)), a))
    print("Error")
    end
end

evcon(c, a) =
  s_eval(caar(c), a) == "t" || s_eval(caar(c), a) ?
  s_eval(cadar(c), a) : evcon(cdr(c), a)

evlis(m, a) =
  s_null(m) ? nothing :
  cons(s_eval(car(m), a), evlis(cdr(m), a))


# REP (no Loop): s_rep
s_rep(e) = s_string(s_eval(s_read(e), "()"))

function s_readlines()
  r = ""
  i = readline()
  while i != ""
    r = r * i
    i = readline()
  end
  return r
end
```

##解説

* リスト処理：`cons` `car` `cdr` `eq` `atom`
[先の記事](https://qiita.com/ytaki0801/items/a11c8143c5c63a406c7f)より，ほぼそのまま抜粋．

* S式入力：`s_read`
新規に作成．字句解析部`s_lex`は`()`および`'`の識別でひとつの文字列を配列化，抽象構文木生成部`s_syn`は括弧ネスト・ドット対・クォート記号対応とし，リスト処理関数でコンスセルによる構文木を生成．それらをまとめたS式入力関数`s_read`を定義．

* S式出力：`s_string`
新規に作成．内部では`nothing`である空リストは`()`を，真偽値は`t` `nil`を出力するよう設定．

* 評価器：`s_eval`＋ユーティリティ関数
["McCarthy's Original Lisp"](http://paulgraham.com/lispcode.html)をベースに`s_eval`関数およびユーティリティ関数を作成．

* REP (no Loop)：`s_rep`
`s_read`→`s_eval`→`s_string`をまとめた`s_rep`を定義．また，そのままでは二重引用符で囲んだ（LISP記述としての）文字列を複数行に分けて入力・記述することができないため，空行を入力するまで行単位の文字列入力を行い，結合して返す関数`s_readlines`を併せて定義．


#備考

##記事に関する補足
* 評価器のみで約60行／1800バイトほど．全体で約160行／3400バイトほどなので半々だろうか．~~ただ，多くの関数をラムダ式代入形式にして相応に字数を減らしているという話が．~~→判断分岐の多くを&&表現に書き直して更に削減．
* Juliaでは，文字列連結が（`+`ではなく）`*`，NULL相当が`nothing`，ということを今回の実装で初めて知った…．

##更新履歴
* 2020-09-21：ソースコードの判断分岐の多くを&&表現に書き直し（Twitter返信より）
* 2020-09-20：初版公開