5

私はSchemeが初めてで、関数内に表示される特定の値が複数の用途でどのように保持されるかを理解しようとしています。次のカウンターを取ります。

(define count
   (let ((next 0))
     (lambda ()
       (let ((v next))
         (set! next (+ next 1))
         v))))

私が理解できない (そしてどこにも説明されていない) のは、使用さnextれるたびに が 0 にリセットされない理由countです。

4

6 に答える 6

12

これはクロージャーと呼ばれます。nextプログラム全体で の バージョンは 1 つだけです。

これをより明確にするために、次のプログラムを検討してください。

(define next 0)

(define count
  (lambda ()
    (let ((v next))
      (set! next (+ next 1))
      v))))

これで、1 つしかないことは明らかですnext

あなたが書いたバージョンは異なってletlambdaますnext. しかし、まだ 1 つしかありませんnext。これに変更した場合、代わりに:

(define count
  (lambda ()
    (let ((next 0))
      (let ((v next))
       (set! next (+ next 1))
       v))))

次に、 の宣言がにあるnextため、毎回の新しいバージョンを作成します。これは、が呼び出されるたびに発生することを意味します。nextlambdalambda

于 2012-05-31T17:17:36.850 に答える
5

サムの優れた答えに追加することが1つあります。あなたの質問は、この動作が「let」と関係がある可能性があることを示唆しています。そうではありません。「let」を使わずに同様のことを行う例を次に示します。

#lang racket

(define (make-counter-from counter)
  (lambda ()
    (set! counter (+ counter 1))
    counter))

(define count (make-counter-from 9))

(count)
(count)

教訓(ある場合):はい!突然変異は紛らわしいです!

編集:以下のコメントに基づいて、突然変異のある言語にどのような種類のメンタルモデルを使用できるかについての洞察を本当に探しているようです。

ローカル変数の突然変異を伴う言語では、引数を値に置き換える単純な「置換」モデルを使用できません。代わりに、関数を呼び出すたびに、後で更新できる新しい「バインディング」が作成されます (別名「変異」)。

したがって、上記のコードでは、「make-counter-from」を 9 で呼び出すと、「counter」変数を値 9 に関連付ける新しいバインディングが作成されます。このバインディングは、「counter」変数のすべての使用に対してアタッチ/置換されます。ラムダ内のものを含む、関数の本体。関数の結果は、この新しく作成されたバインディングへの 2 つの参照を「閉じた」ラムダ (関数) になります。必要に応じて、これらをヒープ割り当てオブジェクトへの 2 つの参照と考えることができます。これは、結果の関数を呼び出すたびに、このオブジェクト/ヒープへの 2 つのアクセスが発生することを意味します。

于 2012-05-31T19:39:55.423 に答える
2

私はあなたの説明に完全には同意しません。関数の定義は1回だけ評価されますが、関数自体は呼び出されるたびに評価されます。

私が同意しない点は、関数が1回だけ定義される(明示的に上書きされない)ため、「...定義を書き換える...」です。

私はそれを次のように想像します。スキーム内の変数のいわゆる字句バインディングにより、スキームインタープリターは、関数定義の評価中に、関数定義の環境で定義された変数、つまり変数「next」があることに気付きます。したがって、関数定義だけでなく、変数「next」の値も記憶します(これは、関数定義とそれを囲む環境の2つを格納することを意味します)。関数が初めて呼び出されたとき、その定義は、格納された環境内のスキームインタープリターによって評価されます(変数「next」の値は0であり、値はインクリメントされます)。2回目に関数が呼び出されると、まったく同じことが起こります-同じ関数定義がその囲んでいる環境で評価されます。ただし、今回は、環境が変数「next」に値1を提供し、関数呼び出しの結果は1になります。

簡単に言うと、関数(定義)は同じままで、変化するのは評価環境です。

于 2012-06-02T01:10:48.237 に答える
2

あなたの質問に直接答えるには、「使用れるたびnextにリセットされません」0count

(define count (let ((next 0))
                 (lambda ()
                    (let ((v next))
                       (set! next (+ next 1))
                       v))))

同等です

(define count #f)

(set! count ( (lambda (next)              ; close over `next`
                 (lambda ()                  ; and then return lambda which _will_
                    (let ((v next))          ;   read from `next`,
                       (set! next (+ v 1))   ;   write new value to `next`,
                       v)))                  ;   and then return the previous value;
              0 ))                        ; `next` is initially 0

(これは「let-over-lambda」パターンです。その名前の Lisp の本さえあります)。

割り当てる値count は一度だけ「計算」されます。この値は、のバインディングを参照するクロージャーnextであり、その (バインディング) はその (クロージャー) の外部にあります。次に、が「使用」されるたび countに、つまり参照先のプロシージャが呼び出されるたびに、プロシージャ (プロシージャ) はそのバインディングを参照します。最初にバインディングから読み取り、次に内容を変更します。ただし、初期値にリセットしません。バインディングは、その作成の一部として 1 回だけ開始されます。これは、クロージャの作成時に発生します。

このバインドは、このプロシージャからのみ表示されます。クロージャーは、このプロシージャーと、このバインディングを保持する環境フレームのバンドルです。このクロージャーは、式のレキシカル スコープ内で式(lambda () ...)を評価した結果です。next(lambda (next) ...)

于 2014-02-27T16:51:35.433 に答える
0

わかりました、私はひらめきのようなものを持っていました。私の混乱は、定義と手続きの違いに関係していると思います ( )lambda : 定義は 1 回発生しますが、手続きは実行されるたびに評価されます。元の関数では、ゼロに設定しletてプロシージャを定義しnextます。その定義は 1 回だけ行わset!れますが、プロシージャ内で使用すると定義がさかのぼるように書き換えられます。したがって、 を使用するたびに、インクリメントさcountれた関数の新しいバージョンが生成nextされます。

これが完全にオフベースである場合は、私を修正してください。

于 2012-06-01T19:20:45.353 に答える
0

以下は、まったく同じことを行うプログラムです。

(define count
  (local ((define next 0)          
          (define (inc)
            (local ((define v next))
                    (begin 
                      (set! next (+ next 1))
                      v))))
    inc))

 (count)   0
 (count)   1
 (count)   2 .........

おそらく、let/set シーケンスは同じメカニズムに従います。実際に呼び出しているプロシージャは inc であり、 count ではありません。inc プロシージャは毎回nextをインクリメントします。同等の定義は次のとおりです。

 (define next 0)

 (define (inc)
  (begin
    (set! next (+ next 1))
    (- next 1)))

 (define count inc)


 (count)  0
 (count)  1
 (count)  2......

したがって、最初のプログラムで count を呼び出すのは、2 番目のプログラム全体を実行するのと同じだと思います。知らない。私もスキームは初めてです。有益な投稿をありがとう。

于 2012-06-04T02:32:57.190 に答える