2012-03-01 48 views
6

我看到下面的代碼...第一次調用(next-num)返回1,第二次返回2函數中的變量

(define next-num 
    (let ((num 0)) 
    (lambda() (set! num (+ num 1)) num))) 

(next-num) ; 1 
(next-num) ; 2 

我無法理解的是... numlet創建內部next-num,這是怎樣的一個局部變量的...怎麼方案知道每次next-num被調用時,的num值沒有被let ((num 0))擦除;計劃如何知道它總是與調用next-num時我們修改的num相同?

看來,num是本地和靜態的...我們如何定義一個局部變量,但不是靜態的?

回答

9

這是「詞法閉包」,你說得對,num的「封閉變量」類似於一個靜態變量,在C例如:這只是可見的代碼let表單中(它的「詞彙範圍「),但它在整個程序運行過程中保持不變,而不是在每次調用函數時重新初始化。

我覺得你對此感到困惑的部分是這樣的:「num是通過在next-num之內創建的,它是一種局部變量」。這是不正確的,因爲let塊不是next-num函數的一部分:它實際上是一個表達式,它創建並返回隨後綁定到next-num的函數。 (這與C完全不同,在C中,函數只能在編譯時創建並在頂層定義它們。在Scheme中,函數是像整數或列表這樣的值,任何表達式都可以返回)。

這裏是另一種方式來寫(幾乎)這使得它更清晰的define只是關聯next-num爲函數返回表達式的值同樣的事情:

(define next-num #f) ; dummy value 
(let ((num 0)) 
    (set! next-num 
     (lambda() (set! num (+ num 1)) num))) 

重要的是要注意的區別

(define (some-var args ...) expression expression ...) 

這使得some-var其執行所有expressions調用時的函數,

(define some-var expression) 

它將some-varexpression的值相結合,然後在那裏和那裏進行評估。嚴格地說,前一個版本是不必要的,因爲它相當於

(define some-var 
    (lambda (args ...) expression expression ...)) 

您的代碼幾乎與此相同,並增加了詞法範圍的變量,num,各地lambda形式。

最後,這是閉合變量和靜態變量之間的一個關鍵區別,它使閉包變得更加強大。反而如果你寫了以下內容:

(define make-next-num 
    (lambda (num) 
    (lambda() (set! num (+ num 1)) num))) 

然後每次調用make-next-num將創建一個匿名函數與一個新的,不同的num變量,它是私有的該功能:

(define f (make-next-num 7)) 
(define g (make-next-num 2)) 

(f) ; => 8 
(g) ; => 3 
(f) ; => 9 

這是一個非常酷且強大的技巧,它解釋了詞彙封閉式語言的很多功能。

修改爲添加:您可以要求Scheme如何「知道」調用next-num時要修改哪個num。總的來說,如果不是在實施中,這實際上很簡單。 Scheme中的每個表達式都是在變量綁定環境(查找表)的上下文中進行評估的,這些變量綁定是名稱與可以保存值的地方的關聯。每個對let表單或函數調用的評估都會通過使用新綁定擴展當前環境來創建新環境。爲了安排lambda表單作爲閉包,該實現將它們表示爲由函數本身加上其定義的環境組成的結構。然後通過擴展定義函數的綁定環境來評估對該函數的調用 - 而不是它調用的環境。

舊的Lisp(包括的Emacs Lisp直到最近)有lambda,而不是詞彙範圍,所以雖然你可以創建一個匿名函數,調用它們將在調用環境,而不是定義環境進行評估,因此沒有關閉。我相信Scheme是第一個正確的語言。 Sussman和Steele最初的Lambda Papers關於計劃的實施對於任何想要了解範圍界定以及其他許多事情的人來說都是非常有意思的擴展閱讀。