我看到以下代码... (next-num)
的第一次调用会返回1
,第二次调用会返回2
。
(define next-num
(let ((num 0))
(lambda () (set! num (+ num 1)) num)))
(next-num) ; 1
(next-num) ; 2
我无法理解的是...... num
是由let
内的next-num
创建的,它是一种局部变量......计划如何每次都知道{ {1}}被调用,next-num
的值不会被num
删除;方案如何知道,只要let ((num 0))
被调用,我们就会修改它num
?
似乎next-num
既是本地的又是静态的...我们如何定义局部变量,而不是静态?
答案 0 :(得分:9)
这是“词法闭包”而你是正确的num
,“闭合变量”类似于静态变量,例如在C中:它只对{{1}内的代码可见form(它的“词法范围”),但它贯穿整个程序运行,而不是每次调用函数时重新初始化。
我认为你感到困惑的部分是:“let
是由num
内部创建的,它是一种局部变量”。这不是真的,因为next-num
块不是let
函数的一部分:它实际上是一个表达式,它创建并返回然后绑定到next-num
的函数。 (这是非常不同的,例如,来自C,其中函数只能在编译时创建并通过在顶层定义它们。在Scheme中,函数是整数或列表之类的值,任何表达式都可以返回)。
这是另一种写(几乎)相同的东西的方式,这使得next-num
只是将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-var
的值,然后进行评估。严格来说,前一版本是不必要的,因为它等同于
expression
您的代码几乎与此相同,在(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
这是一个非常酷且功能强大的技巧,它解释了词汇封闭语言的很多功能。
编辑添加:您询问Scheme如何“知道”调用(define f (make-next-num 7))
(define g (make-next-num 2))
(f) ; => 8
(g) ; => 3
(f) ; => 9
时要修改哪个num
。概括地说,如果没有实现,这实际上非常简单。 Scheme中的每个表达式都在变量绑定的环境(查找表)的上下文中进行评估,变量绑定是名称与可以保存值的位置的关联。 next-num
表单或函数调用的每个评估都通过使用新绑定扩展当前环境来创建新环境。为了使let
形式表现为闭包,实现将它们表示为由函数本身和定义它的环境组成的结构。然后通过扩展定义函数的绑定环境来评估对该函数的调用 - 不调用它的环境。
较旧的Lisp(包括Emacs Lisp直到最近)有lambda
,但没有词法范围,所以尽管你可以创建匿名函数,但是对它们的调用将在调用环境而不是定义环境中进行评估,因此没有关闭。我相信Scheme是第一种能够做到这一点的语言。 Sussman和Steele关于Scheme实施的原始Lambda Papers为任何想要了解范围界定的人提供了极大的思维扩展阅读。