いかにもLispらしいマクロを書いたので解説。
表題の通り。
LET
まずは仕様の確認から。
Common Lispではローカル変数を作るのにLET
という特殊形式を使います。
(let ((var :value))
var)
=> :VALUE
初期化フォームは省略可能です。
省略された場合NIL
で初期化されます。
(let ((var))
var)
=> NIL
初期化フォームを省略する場合は、カッコも省略できます。
(let (var)
var)
=> NIL
Issue.
プログラミングをしていると、ときに、ローカル変数を宣言だけしておいて、初期化は後で行いたいということだってあります。
そのような場合、初期化前に変数を参照しようとした場合、エラーになって欲しい。
LET
は無条件でNIL
に束縛してしまうので、初期化前のNIL
と初期化後のNIL
とを区別できなかったり、初期化前の参照が動いてしまったりとあまり具合がよくありません。
MAY-LET
そこでそんな状況に相応しいマクロMAY-LET
を書きました。
初期化フォームがある場合は通常のLET
と振る舞いは完全に同じです。
(may-let ((var :value))
var)
=> :VALUE
初期化フォームが省略された場合は暗黙理にNIL
が指定されたものと解釈されます。
これもLET
と同じ振る舞いです。
(may-let ((var))
var)
=> NIL
ただし、カッコまで省略された場合は代入前に参照するとエラーを投げます。
(may-let (var)
var)
=> Error, VAR is not initialized yet.
代入後は期待どおり振る舞います。
(may-let (var)
(setq var 0)
var)
=> 0
一度も代入が行われていない場合、コンパイラはクレームをつけます。
一度でも代入されている場合、コンパイラはたとえ代入前参照があっても黙ったままです。
できればそういった代入前参照もコンパイル時に摘発したいところではありますが、労多くして益少しと判断しました。
実行時に適切なエラーが出るので良しとしています。
Under the hood.
マクロ展開した中身を見ながら解説していきましょう。
(macroexpand-1 '(may-let (var)
(setq var 0)
var)) ; <--- 変数の参照に見えますが、、、
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222)))
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0)
var))
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222))) ; <--- その実、SYMBOL-MACROLETです。
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0)
var))
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222))) ; <--- 実際は関数呼び出しであり、その関数は、、、
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0)
var))
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111)) ; <--- 背後にある変数に束縛されている関数を呼び出しています。
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222)))
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0)
var))
;; 背後にある変数は初期値としてエラーを投げる無名関数が束縛されています。
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222)))
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0) ; <--- 代入が行われると、、、
var))
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW)
(SETQ #:G111 (LAMBDA () NEW))
NEW))
(SYMBOL-MACROLET ((VAR (#:G222)))
(SETQ VAR 0)
VAR)))
(macroexpand-1 '(may-let (var)
(setq var 0)
var))
(LET ((#:G111 (LAMBDA () (ERROR "~S is not initialized yet." 'VAR))))
(FLET ((#:G222 ()
(FUNCALL #:G111))
((SETF #:G222) (NEW) ; <--- SETF関数が呼び出され、
(SETQ #:G111 (LAMBDA () NEW)) ; <--- 背後変数をclosureで更新します。
NEW))
(SYMBOL-MACROLET ((VAR (#:G222)))
(SETQ VAR 0)
VAR)))
よって上記例では、代入後、背後変数には無名関数(lambda () 0)
が入っていることとなります。
コード末尾のVAR
参照(に見える関数呼び出し)ではこの関数が呼び出され、期待どおり0
が返るというカラクリです。
Conclusion.
初期化前のローカル変数を参照するとエラーになって欲しい、というような言語コアの振る舞いに手を入れるのに、Issueを投げたりプルリクを出したりコミュニティの説得という政治活動を行ったり言語仕様そのものがアップデートされるのを待ったりすることなく、さくっと実現できるのがLisp一族の何よりの魅力です。
Discussion