【问题标题】:How are Scheme closures formally defined?Scheme 闭包是如何正式定义的?
【发布时间】:2022-01-23 17:34:01
【问题描述】:

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

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

然后赋值给一个变量

(define bar (foo))

这是按什么顺序评估的?从我所见,当foo 被声明时,它存储了一个指向父环境的指针,并声明了它自己的环境。如果我打电话给(bar),我应该立即在保存的本地环境中替换吗?

【问题讨论】:

  • 闭包是语义问题,而不是语法问题。语法的定义不会显示它们的迹象。
  • @molbdnilo 那么有语义定义吗?我主要只是想知道如何告诉我的解释器“好的,这是一个闭包。”?
  • 据我所知,Scheme 报告中没有定义,但 R. Kent Dybvig 的(因 Chez Scheme 成名)dissertation 中有很多有趣的细节。
  • Metalinguistic Abstraction 中的章节SICP 解释了您所需要的一切。看看make-procedure
  • 看看我的this 回答是否有任何帮助。它以 13-15 行伪代码显示了原始的 McCarthy 的 Lisp,并增加了支持闭包(“funarg 设备”)。

标签: closures scheme racket


【解决方案1】:

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

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

Scheme 是一种静态范围的编程语言。变量的每次使用都与该变量的词法绑定相关联。

在 Scheme 计算过程中创建的所有对象,包括过程和延续,都具有无限范围。

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

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

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

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

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

这个过程会返回一个闭包:

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

但是您可以看到,在这两种情况下,这些事物的行为都直接遵循语言的范围和范围规则:没有特殊的“这是一个闭包”规则。

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

在第二种情况下,调用make-incrementor 会为val 建立一个绑定,然后该绑定会被它返回的函数引用和修改。

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

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

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

同样,规则是简单:您只需查看代码,看看它做了什么。没有特殊的“关闭”案例。


如果您确实需要引起这种情况的形式语义,那么 R7RS 的 7.2 具有该语言的形式语义。

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-25
      • 1970-01-01
      • 2017-08-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多