Scheme 编程/局部作用域
本节定义了局部作用域和一般的变量作用域。
Scheme 使用词法作用域,这意味着变量作用域可以直接从程序文本中看到,并且变量作用域在编译时定义。
Scheme 是块结构化的。这里块是指一段具有开始和结束点的代码行。块定义一个作用域,作用域是变量定义(或绑定)的地方。块可以嵌套。存在外部块和内部块。
顶级作用域是一个特殊的作用域,因为根据定义,它没有外部作用域。顶级作用域也称为全局作用域。任何其他作用域都是嵌套的,并且它们都受外部作用域或作用域的影响。局部作用域与当前作用域相同。
顶级作用域是程序作用域中最外层的范围(块)。
我们在这里定义一个在顶级作用域中的变量并评估它的值
> (define x 1)
> x
1
正式地说,我们可以说变量x
绑定到值 1。一般来说,顶级作用域包含全局绑定,这些绑定可以是过程或数据值。
可以使用let
表达式创建新的作用域(即当前作用域的内部作用域)
> (let ((y 2))
> y)
2
let
表达式的值是let
主体内的最后一个评估的表达式。在这种情况下,我们得到y
的值,它为 2。
变量y
只存在于内部作用域中
> y
;; Error: unbound variable: y
但是,来自顶级作用域的变量对所有内部作用域可见
> (define x 1)
> (let ((y 2))
> (+ x y))
3
可以在内部作用域中使用相同的变量名。即使内部作用域中的变量与外部作用域中的变量具有相同的名称,它们也是不同的绑定,并且它们存储在不同的位置。Scheme 从找到变量标识符的最内部作用域使用变量绑定。
因此,我们可以遮蔽外部作用域中的变量
> (define x 1)
> (let ((x 7))
> x)
7
> x
1
内部x
只存在于let
表达式中,但外部x
在let
表达式之前和之后存在。
块的嵌套深度没有限制
> (define x 1)
> (let ((x 7))
> (let ((x 13))
> x))
13
> x
1
请注意,上面的代码只演示了作用域的嵌套,一般来说没有多大意义。
到目前为止,我们已经引用了来自正常表达式的变量,这些表达式产生数据值。但是,我们也可以从过程引用外部作用域中的变量。词法作用域规则对过程的应用方式与对其他表达式的应用方式相同。实际上,let
可以表示为lambda
的语法扩展,因此实际上我们一直都在引用过程中的变量。
当过程引用一个不在过程本身(即在外部作用域中绑定)中绑定的变量时,就会创建一个闭包。外部变量成为闭包的一部分。这种连接非常强大,即使变量的作用域已经过期,它仍然存在于闭包中。从某种意义上说,闭包打破了变量的正常块作用域。当闭包是从内部作用域传递到外部作用域的值时,就会发生这种情况。
这里有一个非常简单的闭包示例
> (define x 0)
> (define inc-x (lambda ()
(set! x (+ x 1))
x))
> (inc-x)
1
> (inc-x)
2
inc-x
每次评估时都会将x
增加 1。该过程不接受任何参数。inc-x
引用外部变量x
。此外,还值得注意的是它也引用了+
。+
实际上是 Scheme 中的一个变量,它恰好绑定到一个将数字加在一起的过程。
当 lambda 引用无法在外部访问的变量时,闭包更有用。你可以使用 let 在 lambda 上做到这一点
> (define inc (let ((x 0))
(lambda ()
(set! x (+ x 1))
x)))
> (inc)
1
> (inc)
2
> x
;; x is unbound variable
这里与前面的例子相同,lambda 关闭了 x 变量,但是这次它在 let 内部,因此它无法在外部访问。
闭包可以以多种方式使用。高阶函数将其他函数作为参数并应用它们,参数可以是闭包。闭包也可以用来模拟对象。它们在外部变量中维护状态,并在每次应用时更新状态。
这是一个示例,其中闭包与高阶函数map
一起使用
> (define x 1)
> (map (lambda (num)
(+ num x))
'(10 11 12))
(11 12 13)
map
接受两个参数:一个过程和一个列表。该过程应用于列表中的每个元素,并根据过程应用的结果创建一个新列表。
此示例中涉及两个作用域:顶级作用域和lambda
创建的内部作用域。num
存在于lambda
的局部作用域中,但x
不存在。因此,x
是lambda
创建的闭包的一部分。map
是正常表达式的一部分,并且它不会启动新的作用域。