木の実を埋めなかったので拾われないLisp

TL; DR

 Lispをつくろうとして失敗しました。ちーん。

どんなものをつくろうとしたか

 Common Lispのすごく小さなサブセットをつくろうとしました。それをつくることで、普段使って理解した気になっているCommon Lispのパッケージやリードテーブル、ひいては実行モデル等を理解するのが目的でした。
 機能としてはなんとなく、以下のようなことを妄想していました:

  • Lisp-2
  • CLOSなし
  • コンディションなし
  • loopなし
  • 文字列あり(Unicode文字列)
  • リストあり
  • 関数よびだしあり
  • パッケージあり
  • リードテーブルつきのreadあり
    • したがって簡単なリーダマクロ
  • レキシカル環境あり
  • eval
  • マクロ展開

実際にはどんな産物ができたか

これです。

github.com

機能的には

  • Lisp-2
  • 組込み関数あり
  • ユーザ定義関数はなし
  • リードテーブルなしのread
    • 関数をちゃんと作れなかったので
  • パッケージあり(切り替えられないけど)
  • いちおう簡易printがある
  • 環境の構造がちょっとおかしい?
  • REPLがある

 関数呼び出しが実装できない気がして気が遠くなってきたので、いったん一区切りつけることにしました。

敗因はなんだったのか

 環境(グローバル/レキシカル)やパッケージ、そしてシンボルのスロットについての理解が誤っていたことが原因でした。データ構造の設計に誤りがあるのです。

nutslispではパッケージと環境はそれぞれ以下のように定義されています。

type
  LispPackage* = ref object of LispT
    name*: string
    nicknames*: seq[string]
    environment*: LispEnvironment

  LispEnvironment* = ref object of LispT
    parent*: LispEnvironment
    binding*: TableRef[LispObjectId, LispSymbol]

「パッケージがグローバル(トップレベル)環境である」「パッケージはシンボルのテーブルをもつ」という認識と、「環境はその親環境を持ちうる(レキシカル環境の一つ外の環境)」「シンボルのvalueスロットに値を持つ(これは環境においてもそうするものだ)」という認識で、この構造にしました。ちなみに、Nimの言語上の制約から、自前定義した型(クラス)をテーブル(Nimにおけるハッシュテーブル)のキーにすることができません。そういった事情もあり、パッケージが保持するシンボルテーブルも兼ねて、bindingLispObjectのIDからシンボルへのテーブルになっています。

 でも、これでは(レキシカル)環境のシンボルに束縛した値を得るときどうするのでしょう。シンボルのスロットには、パッケージのトップレベルの値が入っているはずです(たとえば(setf hoge "mojiretsu")としたときの値)。シンボルをレキシカルな環境の値や関数保持用の構造として利用すると、トップレベルの値が上書きされて消えてしまいます。

 さあさあ、てえへんだ。

おわりに

 正しくはどうあるべきか、ひいては環境やパッケージやシンボルとは何であるのか、についてはまだ不明です。Hyperspec読書大会を引き続きひとり開催してなんとか理解を深めたいところです。

 おそらくありがちなところで盛大に転んでしまったというところなんでしょう。目標の、リードテーブルや関数の実行モデルやあれやこれやの理解を深めること、はまだまだ先が長そうですね。