closed-let
letで束縛された変数は外からは見えないが、関数を介してアクセスすることが出来る。定義されたときの変数を参照するのがレキシカルクロージャの特徴である。
(defun f1 () (let ((foo 100)) (values (lambda (n) (+ foo n)) (* foo foo)))) (f1) | #<lexical-closure: (anonymous)> 10000 (funcall (setq x (f1)) 100) | 200
この変数がスペシャル変数になると、もはや外からはアクセスできなくなり、グローバルに定義された値か、実行された環境での値を取る。ダイナミックな動作をする。
(defvar foo 0) | foo (f1) #<lexical-closure: (anonymous)> 10000 (funcall x 100) | 100 (let ((foo -100)) (f1)) | #<lexical-closure: (anonymous)> 10000 (let ((foo -100)) (funcall x 100)) | 0
一度スペシャル変数にした変数は、環境をリセットでもしないと元に戻らない。レキシカルクロージャの局所変数を外で用いる使い方をしていた場合、その変数がスペシャル変数になってしまうと動作に支障をきたす。もし、システムの重要な位置を占めていたら再起動しなくてはならない。
そんなわけで、局所変数がスペシャル変数にならないように色々な工夫がされる。*hoge*のような、*で括った変数しかスペシャル変数にしない。普段いじらないようなモジュールに分ける。こういった方法は変数の使い勝手をそのままに、スペシャル変数に設定されるのを避ける良い方法である。とはいえ、故意に設定することはできるため、信頼性に欠ける。
もう一つの方法は、変数名を外から見えないようにするやり方である。変数名を隠蔽すれば、その変数をスペシャル変数に設定することはできないわけだ。(gensym)で作られたシンボルはインターンされないため、たとえ同じ名前の変数をスペシャル変数にしても、クロージャに影響を与えることはない。
(defmacro clet-1 (let-type let-form &rest args) (let ((defs (mapcar (lambda (x) `(,(car x) ,(gensym))) let-form)) (f (lambda (x y) (subst (cadr y) (car y) x)))) `(,let-type ,(reduce f defs :initial-value let-form) ,@(reduce f defs :initial-value args)))) (defmacro def-clet (let-type) `(defmacro ,(intern (concat "c" (symbol-name let-type))) (let-form &rest args) `(clet-1 ,',let-type ,let-form ,@args))) (series 1 def-clet let let*) (defun f2 () (clet ((foo 100)) (lambda () foo))) (setq y (f2)) | #<lexical-closure: (anonymous)> (funcall y) | 100
clet・・・closed-letを使って束縛される変数は、(gensym)の変数に置き換えられる。置き換えにはオブジェクト名をそのまま使うため、このあとで外から同じ名前の変数を入れても同じ変数にならない。これが問題になるのは、別のマクロと組み合わせたときである。
(defmacro m1 () `(lambda () bar)) (defun f3 () (let ((bar 2)) (m1))) (funcall (f3)) | 2 (defun f4 () (clet ((bar 2)) (m1))) (funcall(f4)) | 変数が定義されていません: bar (defun f5 () (clet ((bar 3)) (macrolet ((m2 () `(lambda () bar))) (m2)))) (funcall (f5)) | 3
外からその変数を注入しない場合や、cletの内側に変数を送り込むようなマクロであれば問題はないので、これは信頼性とのトレードオフだろう。