bit 1982年05月号『Prolog入門(2)』に Portable PrologというLISPで書かれた小さなProlog処理系が載っている。 “Portable” という名前の通り色々な環境で動かすことを前提としており、 ほぼLISP 1.5の機能のみで書かれているため移植が簡単にできるようになっている。 これを少し書き換えてCommon Lispで動かした。
今回は「もとのプログラムを極力書き換えない」という方針で移植することにした。
コードを読んでいると prog
を let
に書き換えたり、
cond
を when
に書き換えたりしたくなるような箇所が多々あるが、
そういった気持ちをぐっとこらえ、最小限の変更のみを行った。
もとのコードが150行ほど。 これに対して変更したのが10行。新たに追加したのが3行。 書き換えた箇所の大部分はそもそもLISP 1.5にない機能を使っているところ、 つまり必ずしも portable とはいえないところなので、変更するのもしかたない。 極力書き換えないという方針は十分に守れたと思う。
ちなみに、あまりにももとのコードが残りすぎているので ソースコードをここには載せないことにした。 読みたい人はAmazonなどでbitを買った上で以下の変更点を見てもらいたい。
もとのコードはLISP 1.5なのでダイナミックスコープを使っている。
Common Lispで実現するのは簡単で、単に special
と宣言すれば良い。
(DECLAIM (SPECIAL
FETCHED-SUBST EPILOG NEW-SUBST CLAUSE OLD-SUBST CUE UNDO-LIST))
宣言すべき変数はコードを読めばわかるが、 SBCLであれば関数をコンパイルした際に出る警告(undefined variable) を読めば確実に対応できる。
もとのコードでは属性リストを書き換えるために PUTPROP
を使っているが、
Common Lispでは (setf (get ...) ...)
を使うことになる。
ちなみにLISP 1.5には PUTPROP
はない。
純粋なLISP 1.5だと PROP
を使って自分で定義する必要がある。
Portable Prologでは先頭に *
が付くシンボルが(Prologの)変数になるのだが、
これを実現するために(LISPの)シンボルの先頭の文字を取り出す必要がある。
もとのコードでは CHARACTER
という関数を使っていたが、
Common Lispでは symbol-name
と aref
あたりを使うことになる。
ちなみに CHARACTER
はLISP 1.5にはない。
純粋なLISP 1.5だと UNPACK
を使うことになると思う。
Portable Prologでは +
と -
という記号が使われるが、
古いLISPでは+と-から始まるアトムは数になってしまうため、
'/+
とか '/-
といったように /
でエスケープしている。
Common Lispでは /
を取って '+
とか '-
と書けば良い。
ちなみに、この /
はMACLISPなどの機能でLISP 1.5にはない。
純粋なLISP 1.5だとおそらく '$$*+*
のように書くのだと思う。
(MAPCAR (CDDR FORM) '(LAMBDA ...))
のようなコードがあるが、
これはCommon Lispだと (mapcar #'(lambda ...) (cddr form))
のように
引数の順番が入れ替わる。また、 '
ではなく #'
を使う。
ちなみにLISP 1.5にはMAPCARはない。
純粋なLISP 1.5だと自分で定義する必要がある。
Portable Prologでは節の終わりに :
を置く。
Common Lispのreadで :
を読むとリーダマクロが動いてしまう。
リードテーブルをいじってもいいのだが面倒なので、
コロンではなく別の記号を使うことにした。
前述の通り、+, -, * といった記号が他に使われており、
どことなく四則演算を感じるので /
を使うことにした。
節の終わりに /
を書くのは少々気持ち悪いが大した問題ではないだろう。
Portable Prologの (PROLOG)
という関数を呼ぶと、
READ
と PRINT
を繰り返すのだが、
Common Lispでこれをやると、 print
のあとに改行されずに read
が動き出し、
非常に見栄えが悪い。そこで、 read
の直前に terpri
を入れてやることにした。
というわけで書き換えたコード動かした。
Portable Prologの文法は癖があり、
先頭が +
なら節の定義、先頭が -
なら質問となり、
頭部、各ゴールは -
で区切る。
終端には本来 :
が付くが、前述の通り /
を代わりに使っている。
変数は先頭に *
が付く。
CL-USER> (prolog)
(PORTABLE PROLOG (IN LISP))
;;; failを定義するために = を定義する
;;; eq(X, X).
+(= *x *x)/
DEFINED
;;; バックトラックを見るためにfailを定義する
;;; fail :- eq(1, 2).
+(fail) -(= 1 2)/
DEFINED
;;; append([], X, X).
+(append () *x *x)/
DEFINED
;;; append([X|Xs], Ys, [X|Zs]) :- append(Xs, Ys, Zs).
+(append (*x . *xs) *ys (*x . *zs)) -(append *xs *ys *zs)/
DEFINED
;;; appendを呼んでみても成功したということしか分からない
-(append (a b) (c) *z)/
*S*
;;; callでLISPのprintを呼ぶと変数*zが見れる
-(append (a b) (c) *z) -(call print *z)/
(A B C)
*S*
;;; appendを逆向きに使う
-(append *x *y (a b c)) -(call print *x) -(call print *y)/
NIL
(A B C)
*S*
;;; すべての組み合わせを見るためにfailを使う
-(append *x *y (a b c)) -(call print *x) -(call print *y) -(call terpri) -(fail)/
NIL
(A B C)
(A)
(B C)
(A B)
(C)
(A B C)
NIL
NIL
;;; 終了
-(end)/
EPILOG
CL-USER>
もともと移植しやすいように書かれたコードだけあって、 かなり簡単にCommon Lispで動かせた。 レキシカルスコープを採用しながらも、 宣言一つでダイナミックスコープを使える Common Lispの懐の深さも十分に感じることができて楽しかった。
2022-01-26