4

変数またはバインディングという用語がどのように使用されているかについて、私は混乱しています/確信が持てません。私の不確かさは、関連する 3 つの簡単な質問に要約できると思います。

(let ((hi 'outer))
  (let ((hi 'inner))
    (print hi))
  (print hi))

質問 A: 上記のコードで、次のうち正しいものはどれですか?

  1. 変数は 1 つだけです。1 つの変数には、外部バインディングと内部バインディングの 2 つのバインディングがあります。

  2. 1 つの名前の 2 つの変数があります: 外部変数と内部変数です。

インターネットでローカル変数について読んだとき、記事では 1 を選択しているように見えることもあれば、2 を選択しているように見えることもあります。

(let ((hi 0))
  (print hi)
  (setq hi 1)
  (print hi)
  (setq hi 2)
  (print hi))

問題 B: 上記のコードに当てはまるのは次のうちどれですか?

  1. 再利用されているバインディングが 1 つあります。

  2. 3つのバインディングがあります。

私はバインディングという言葉を使って答えとして 2 を選ぶ資料を見たことがありませんが、その一方で、「hi という名前は 3 回バインドされている。3 回バインディングが発生しました。コードは 3 つのバインディングを行います。 " よくわかりません。

(defun fac (n)
  (if (> n 1)
      (* n (fac (1- n)))
    1))
(fac 4)

質問 C: 再帰関数が実行されているとき、正しいのはどれですか?

  1. 1 つの変数に対して同時に複数のバインディングが存在します。

  2. 同じ名前の複数の変数が同時に存在します。

これは質問 A に似ているように見えるかもしれませんが、質問 A には 2 つの let フォームが含まれており、それぞれが 1 回だけ実行されます。一方、この質問は、複数のインスタンスで同時に実行される 1 つの let フォームに似ています。

これらの質問は、ピンの頭の角度のようなものですか? ループ内でのクロージャーの使用に関する有名な落とし穴に関する多くの記事があるため、これらの質問について疑問に思っています。これらの記事を理解するには、1 つの変数とは何か、1 つのバインディングとは何かを知る必要があると思います。

4

2 に答える 2

3

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-firstget-second返し、 と の両方がその値set-firstset-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この種のケースでは、たとえば、反復ごとに新しいバインディングを確立するかどうか、または同じバインディングを更新するかどうかを知ることが突然非常に重要になります。次のコードは何を出力する必要があります 109876543210000000000?

(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:

于 2013-09-12T12:55:58.917 に答える