Lars Brinkhoff の回答は、HyperSpec にアピールすることで、これに最も直接的に答えていると思います。Chapter 6. Variables in Peter Seibel's Practical Common Lispも参照してください。
ただし、これをテストするために何ができるかについても考えてみましょう。レキシカル スコープとレキシカル クロージャを備えた言語の利点の 1 つは、同じバインディングをクロージャ間で共有できることです。
複数のクロージャによって参照される 1 つのバインディング
たとえば、1 つx
の変数をバインドし (ここに 1 つしかないことは間違いありませんx
)、それにアクセスする 2 つのクロージャーを返すことができます。
(defun incrementer-and-getter (value)
(let ((x value))
(values (lambda ()
(setq x (+ 1 x)))
(lambda ()
x))))
次に、クロージャーを使用するときに、それらが同じバインディングを参照していることがわかります。
(multiple-value-bind (inc get)
(incrementer-and-getter 23)
(list (funcall get)
(funcall inc)
(funcall get)))
; => (23 24 24)
let
ネストされたsを持つ複数のバインディング
これで、指定したケースにいくつのバインディングがあるかをテストするために、同様のことができます。
(defun test2 ()
(let (get-outer
set-outer
get-inner
set-inner)
(let ((hi 'outer))
(setq get-outer (lambda () hi)
set-outer (lambda (new) (setq hi new)))
(let ((hi 'inner))
(setq get-inner (lambda () hi)
set-inner (lambda (new) (setq hi new)))))
(values get-outer
set-outer
get-inner
set-inner)))
(multiple-value-bind (get-outer set-outer get-inner set-inner)
(test2)
(list (funcall get-outer) ; retrieve outer
(funcall get-inner) ; retrieve inner
(funcall set-outer 'new-outer) ; update outer
(funcall set-inner 'new-inner) ; update inner
(funcall get-outer) ; retrieve outer
(funcall get-inner))) ; retrieve inner
; => (OUTER INNER NEW-OUTER NEW-INNER NEW-OUTER NEW-INNER)
内側と外側のバインディングが異なります。
で更新された単一のバインディングsetq
複数のケースのsetq
場合:
(defun test3 ()
(let (get-first
set-first
get-second
set-second)
(let ((hi 'first))
(setq get-first (lambda () hi)
set-first (lambda (new) (setq hi new)))
(setq hi 'second)
(setq get-second (lambda () hi)
set-second (lambda (new) (setq hi new))))
(values get-first
set-first
get-second
set-second)))
(multiple-value-bind (get-first set-first get-second set-second)
(test3)
(list (funcall get-first)
(funcall get-second)
(funcall set-first 'new-first)
(funcall get-first)
(funcall get-second)
(funcall set-second 'new-second)
(funcall get-first)
(funcall get-second)))
(multiple-value-bind (get-first set-first get-second set-second)
(test3)
(list (funcall get-first)
(funcall get-second)
(funcall set-first 'new-first)
(funcall set-second 'new-second)
(funcall get-first)
(funcall get-second)))
; => (SECOND SECOND NEW-FIRST NEW-FIRST NEW-FIRST NEW-SECOND NEW-SECOND NEW-SECOND)
ここでは、と の両方が同じ値get-first
をget-second
返し、 と の両方がその値set-first
をset-second
更新します。クロージャーは同じバインディングで閉じます。
関数を呼び出すたびに、新しいバインディングが確立されます
再帰的なケースでは、少しこっそりする必要がありますが、それでも確認できます。
(defparameter *closures* '())
(defun recurse (n)
(push (lambda () n) *closures*)
(push (lambda (new) (setq n new)) *closures*)
(unless (zerop n)
(recurse (1- n))))
(recurse 1) ; put four closures into *closures*
これで、それらを元に戻して、何が起こるかを確認できます。
;; remember we pushed these in, so they're in backwards
;; order, compared to everything else we've done.
(destructuring-bind (set-y get-y set-x get-x)
*closures*
(list (funcall get-x)
(funcall get-y)
(funcall set-x 'new-x)
(funcall set-y 'new-y)
(funcall get-x)
(funcall get-y)))
; => (1 0 NEW-X NEW-Y NEW-X NEW-Y)
関数の呼び出しごとに新しいバインディングがあるため、クロージャーは異なるバインディングを参照します。
反復構造におけるよくある混乱
それだけの価値があるため、この動作に慣れることはそれほど難しくありません (そもそも驚くべきことである場合)。しかし、経験豊富な Lisp のベテランでさえ、反復構造の動作につまずくことがあります。do
この種のケースでは、たとえば、反復ごとに新しいバインディングを確立するかどうか、または同じバインディングを更新するかどうかを知ることが突然非常に重要になります。次のコードは何を出力する必要があります 10987654321
か0000000000
?
(let ((closures '()))
(do ((x 10 (1- x)))
((zerop x))
(push (lambda () x) closures))
(map nil (lambda (closure)
(print (funcall closure)))
closures))
の場合do
、それ0000000000
は です (強調を追加):
すべてのステップフォームが指定されている場合、左から右に評価され、結果の値がそれぞれの変数に割り当てられます。
これは、loop
実装に依存する場合によく出てきますが、人々は他の反復マクロとは異なる動作も期待しています。たとえば、次の StackOverflow の質問を参照してください。
またはこれらのスレッドcomp.lang.lisp
: