Scheme 的闭包是如何正式定义的?

How are Scheme closures formally defined?

我最近冒险进入了编写 Scheme 解释器的神奇领域,但我 运行 遇到了一个障碍:闭包。据我了解,它们封装了一个本地环境,其中包含一个每次调用闭包时都会恢复的过程(这可能不完全正确)。我似乎无法在网上找到任何地方的问题是闭包是如何正式定义的,即在 EBNF 语法中。我见过的大多数例子都说闭包是一个零参数的过程,它有一个嵌套在 let 表达式中的 lambda 表达式。这是定义 Scheme 闭包的 only 方式吗?更重要的是,如果没有正式的方式来正式定义闭包,您实际上如何解释它?如果将所有 let 表达式转换为 lambda 会发生什么?例如,如果我这样声明闭包

(define (foo) (let ((y 0)) (λ (x) (…))))

然后赋值给一个变量

(define bar (foo))

这是按什么顺序计算的?据我所见,当 foo 被声明时,它存储了一个指向父环境的指针,并声明了它自己的环境。如果我调用(bar),我是否应该立即在保存的本地环境中替换?

今天,我认为将闭包视为某种特殊的神奇事物没有帮助:很久以前,在 Scheme 史前的语言中,它们是,但在现代语言中,它们根本不是什么特殊的东西:他们只是以一种明显的方式遵循语言的语义。

两个重要的事情(这些都是引自 R7RS,都来自第 1.1 节:

Scheme is a statically scoped programming language. Each use of a variable is associated with a lexically apparent binding of that variable.

All objects created in the course of a Scheme computation, including procedures and continuations, have unlimited extent.

这意味着 Scheme 是一种具有词法范围和无限范围的语言:只要存在引用的可能性,任何变量绑定就存在。而且,方便的是,您始终可以静态地(即通过阅读代码)告诉一些代码可能引用的绑定。

重要的是这些规则非常简单:没有奇怪的特殊情况。如果对变量绑定的引用在一段代码中可见(您可以通过查看代码来判断),那么它就是可见的。它只是有时不可见,或仅在某些间隔期间,或仅当月亮凸出时才可见:它是可见的。

但这些规则的含义是,过程需要以某种方式记住它们引用或可能引用的所有绑定,以及它们在创建时在范围内的所有绑定。因为范围是静态的,所以始终可以确定哪些绑定在范围内(免责声明:我不确定这对于全局绑定是如何正式工作的)。

因此,非常老式的闭包定义将是在其引用的绑定存在的范围内定义的过程。这将是一个闭包:

(define x
  (let ((y 1))
    (λ (z)
      (set! y (+ y z))
      y)))

并且此过程将 return 关闭:

(define make-incrementor
  (λ (val)
    (λ ()
      (let ((v val))
        (set! val (+ val 1))
        v))))

但是你可以看到,在这两种情况下,这些东西的行为都直接遵循语言的范围和范围规则:没有特殊的 'this is a closure' 规则。

在第一种情况下,以 x 的值结束的函数既引用并改变了 y 的绑定,也引用了 z 已建立的绑定当它被调用时。

在第二种情况下,调用 make-incrementorval 建立一个绑定,然后该绑定被它 return 的函数引用和改变。

我不确定将所有 let 变成 λ 是否有助于理解事物,但第二件事变成了

(define make-incrementor
  (λ (val)
    (λ ()
      ((λ (v)
         (set! val (+ val 1))
         v)
       val))))

现在您可以看到,由 make-incrementor 编辑的函数 return 在调用时立即调用另一个函数,该函数仅将 v 绑定到它的参数,它本身就是make-incrementor 建立的绑定的值:这样做只是为了保持绑定的预增量值当然。

同样,规则简单:您可以只看代码,看看它做了什么。没有特殊的'closure'情况。


如果您真的想要产生这种情况的形式语义,那么 R7RS 的 7.2 具有该语言的形式语义。

闭包是一对指向某些代码的指针和指向应在其中计算代码的环境的指针,这与创建闭包的环境相同。

语言中闭包的存在使环境看起来像一棵树。没有闭包,环境就像一个堆栈。这就是第一个 lisp 系统中的环境。 Stallman 说他在 elisp 中选择了动态环境,因为静态环境在当时(1986 年)很难理解。

闭包是最核心的计算概念之一,它们允许派生出许多其他概念,例如协程、纤程、延续、线程、thunks to delay 等。