Lisp開発スレ
7年前に一度挫折した Lisp 開発を再開しました(もう再開して、ひと月以上たってるのに今更)
ポリシーは次のとおり
- Go言語で開発し、Go言語アプリケーションに組み込める小さな処理系とする
(GopherLua みたいな) -
Common LispISLisp と autolisp の最大公約数的な仕様とする
参考にしたページ
結構、リファレンスとしては良い。このページに記載の機能で未実装な機能:
- (format)
- 分数・複素数・number型(ただし、integerとfloatはある)
(first)(second)(third)(rest)(find)、(member)はあるが :test パラメータ未対応- 配列、ベクトル、ハッシュテーブル
- 構造体、クラス
- 多値代入
(eq)(eql) (イコールは (equal)(equalp) しかない。(equalp) は英大文字・小文字や/実数・整数を区別しないだけ)(incf)(decf)- (type-of)(describe)(deftype)
(when)(unless)(case)- (loop)
(do) - オプション引数・
キーワード引数・補助パラメータ(ただし &aux のかわりに autolisp と同様に / が使える)、多値返却 - 数学系関数
pi などの組み込み定数例外
目下の課題(1) (setf) を実装したのはよいが、作り方が標準とは違っていた
(setf) とは代入を行う万能関数で
gmnlisp> (defparameter m '(1 . 2))
m
gmnlisp> m
(1 . 2)
gmnlisp> (setf (car m) 3)
3
gmnlisp> m
(3 . 2)
のように、構造の中の値まで書き換えることが出来る。
Common Lisp では (setf (car X) Y)
は (replaca X Y)
いう別関数の置換するマクロとして実装されているらしい(replaca は consセルの car 部分を置換する関数)。
が、gmnlisp では (car) などを右辺値(値そのものとセッターのクロージャ)を返す関数として実装し、(setf) はそういう一部の関数のセッターを呼び出す関数として実装してしまった。
でも、まぁ、問題なく動くわけだし、これはこれでいいか
目下の課題(2)キーワード引数
これを実装しないと、ちゃんとした (with-open-file) が実装できない。これは関数の I/F に改修がいるなぁ
gmnlisp の append 、うっかり、引数のリストそのものに破壊的変更しちゃってたので、直した。
(let ((x '(1 2 3)))
(append x '(4 5 6))→ これで式の全体の値だけでなく、x そのものも (1 2 3 4 5 6) になってしまっていた。
Go言語の雰囲気でやってしまったヨ!
SBCLにて検証:
(let* ((part1 (list 1 2 3))
(part2 (list 4 5 6))
(part3 (list 7 8 9))
(all (append part1 part2 part3)))
(print all)
(setf (nth 1 all) 'aaa)
(print part1)
(setf (nth 4 all) 'bbb)
(print part2)
(setf (nth 7 all) 'ccc)
(print part3)
(print all)
(terpri)
)
$ sbcl --script append.lsp
(1 2 3 4 5 6 7 8 9)
(1 2 3)
(4 5 6)
(7 CCC 9)
(1 AAA 3 4 BBB 6 7 CCC 9)
なるほどー
gmnlisp のコミット 2068edc を動かしていて見つけた問題です。
現時点では gmnlisp はまだ試行錯誤していて仕様に曖昧な部分もあるということは承知しておりますので、あくまでも既存の Lisp 系言語の一般的な仕様に照らしておかしいと感じられる部分や、これもアリだけれど今の内に検討してはっきりさせた方が良いと思った箇所について記述します。
私自身は Scheme 派であり、 Common Lisp の系統にはそれほど明るいわけではないということもお断りしておきます。
トップレベルでのクオート
トップレベルでクオートを付けたリテラルを評価しようとするとエラーになります。
gmnlisp> 'foo
Unbound variable `'`
クオート記号は quote
の短縮表記ですので、この場合は (quote foo)
を評価したときと同じ結果が期待されます。
二重のクオート
トップレベル以外を含めて二重にクオートしたときの解釈がおかしいようです。
gmnlisp> (print ''foo)
print: Too many arguments
''foo
は (quote (quote foo))
の意味に解釈されるべきものですが、どうやら '
という名前のシンボルと foo
という変数のふたつに解釈されてこのエラーになっているように見えます。
真偽値
nil
が小文字で T
が大文字なのは不自然なのでどちらかに統一した方がよいように思います。
gmnlisp> (equal 1 2)
nil
gmnlisp> (equal 1 1)
T
大文字小文字
Common Lisp では識別子の大文字と小文字の区別がありますが、デフォルトではリーダが大文字に変換するという挙動によって表面上は区別がないかのような扱いになります。 ただし、この挙動は変更することもできます。
Common Lisp はリーダのカスタマイズが出来過ぎるので小さな Lisp でカスタマイズ性を真似するべきではないと思いますが、それを脇においても古い Lisp 系言語ではリーダが大文字・小文字を統一しつつ内部的には区別があるという挙動はよくあるものだったようです。
一方で、 Scheme では仕様改定の折に大文字・小文字を区別するように改められましたし、 Clojure でも区別します。 今時は大抵のプログラミング言語で大文字・小文字を区別するので区別することにしておかないと連携するときに困るのです。
現代的な感覚や実用性、アプリケーション組み込み用という立場からは大文字・小文字を区別するのが自然だと思いますが、 Common Lisp や AutoLisp を意識するなら検討する必要があるだろうという意味での背景の説明です。
with-open-file 実装
サポートしたオプション引数は
-
:direction :input
と:direction :output
-
:if-does-not-exist
(← 仕様をちゃんと説明している文書が以外とないが、ファイルが存在しなかった時、引数の値をファイルハンドル用変数に入るみたい)
だけ。普通はオープンエラーは、そのままエラーにしてキャッチするですかね…
それだ!
この autolisp 用の「未使用 / 未宣言の変数を検出するスクリプト」を gmnlisp で動作できるようにするのが一つの目標でした。で、(findfile) とか *error* とかを誤魔化すために以下のスクリプトから呼び出すようにしても、どうしても不適切な「未使用エラー」を出してしまう。
(defun findfile (fname) fname)
(defvar *error* nil)
(load "strict.lsp")
(strict "strict.lsp")
これ、当時、数日とりくんでどうしても分からなかったので、一旦諦めたのですが…
今日ひさしぶりに実行してみると、(princ) で引数の個数のエラーが出るくらいで、うまく動くようになってしまいました。
面倒なので、いまさら原因は調べませんが、おそらくはクォート関連の実装方法を変えたせいでしょうね
とほほのLISP入門の loop
の説明は大幅に省略されていますね。
Common Lisp の loop
は仕様の中で最も複雑な構文として有名で、無限ループのためだけの構文というわけではありません。 様々なループのパターンを扱える DSL です。 loop
は無ければ無くてもプログラミングには十分に足りるのですが、ちょっと複雑な場合をひとつの式に押し込めたいという場合に欲しくなるというのが loop
という構文の立場です。
なのであまり loop
の機能を省略しすぎると他のループ系構文に比べて特に便利というわけでもなくなるので存在価値が小さいですし、フルセットを用意するのはさすがにしんどいでしょうから、どのあたりで妥協するか難しいところですね。
いったん (loop) は仕様から外そうと思います。かわりに ISLisp 系のループ系構文をなんぼか実装する方向で
文字列表現について
- 最初は Go の string (UTF8)をそのまま使っていた
- その後、(setf (aref STRING INDEX) VALUE) の実現の都合、rune配列(UTF32)に変えた
しかしながら、Goのライブラリを使うたびに UTF8 変換をしなきゃいけないのは、効率が辛い。どこかの効率か標準仕様準拠のどこかを諦めなければいけない
お聞きしたリンク
-
Gauche - A Scheme Implementation
- Gauche は UTF8 の方を採用しているらしい。
- UTF8文字列と、UTF32文字列の両方を実装した。
- 内部的には UTF8String(=string) , UTF32String(=[]rune)
- デフォルトは type String = UTF8String だが、type String = UTF32String でもビルドできる
- UTF8String は READONLY で、ランダムアクセス不可(setf (elt "STRING" INDEX) VALUE) のようなことはできない
- (to-utf8 …) , (to-utf32 ...) で相互変換できる
UTF8String は (elt "STRING" INDEX) はできるが、計算量 O(n)
- (eq A B) … A と B はアドレスも含めて同値
- (eql A B) … A と B は値・型ともに同じ。コレクションの場合、要素が一致していても t にはならない(器が違うから?)
- ISLisp/SBCL では (eql (cons 1 2) (cons 1 2)) は nil。現行の gmnlisp では t [要修正]
- (equal A B) … A と B 、構成要素まで同じであれば「同じ」扱い。ただし、整数(2)と実数(2.0)は nil、英大文字・小文字も nil
- ISLisp/SBCL では (equal 2 2.0) , (equal "a" "A") ともに nil。gmnlisp では (equal 2 2.0) は t [要修正]、(equal "a" A") は nil
- (eqlualp A B) … ISLisp には存在しない。SBCL/gmnlisp では (equal 2 2.0) (equal "a" "A") ともに t
- (eql "A" "A") → nil (ISLisp , SBCLともに) 。ISLisp の規格上は処理系定義。gmnlisp では Go の仕様上、スライスは nil としか比較できないので、文字列については (equal) と同じ扱いにせざるをえない
ISLISP で (eql "A" "A")
の結果が処理系定義と書かれているのは文字列リテラルの場合の動作であって文字列一般の動作ではないと解釈するべきです。
それらを(オブジェクトの変更なしに)区別する操作がなく,かつ,一つのオブジェクトの変更がもう一つのオブジェクトを同じように変更する場合に同一とする。
というのが eq
と eql
の挙動です。 数値と文字の場合を除いて eq
と eql
は同じ結果であるべきです。
プログラムテキスト中にリテラルとして含まれる文字列は,変更不可能なオブジェクトとする。
とあるので、リテラルとしての文字列に限ってはオブジェクトを変更した場合という条件が成立しません。 前提条件が成立しないので結果を決められず、処理系にまかせるということです。
これは C で "A"=="A"
としたときに真になる場合があるというのと同じようにリテラルの配置に関しての処理系の裁量を認めていると考えられ、等価性に関しての裁量ではありません。
文字と数値が特別扱いなのは、ポインタより小さなサイズで表現可能な小さな値はヒープアロケーションしてそれへのポインタで表すということをせずに直接的にそこに埋め込むというような最適化をする実装が一般的だからです。 (大きな数値のときにはヒープアロケーションしてシームレスに切り替える。) Ruby とかでもやってますね。
実装の都合が言語仕様に現れるのは不格好なんですが、この特別扱いのために同値判定と同一判定の二種類ではなく間にもうひとつの等価性判定が用意されているんです。
地道に ISLisp の関数を一つ一つ実装してゆくのも飽きてきたし、このあたりで何か実用的なアプリを書いてみたいなということで「S式で Makefile を書く make」を作ってみた。
ビルドルールを S 式で与えるということは、動的にルールを生成することもできるということで、これは結構便利かもしれない。C言語のビルドルールの自動生成もできた。