TL;DR: 闭包 由尾调用优化函数创建的必须捕获其定义环境(相关部分)的副本 .或者,忽略 TCO 部分,将其视为常规递归函数,其中在递归函数执行期间创建的任何 lambda 函数都是一个闭包,捕获它所引用的变量的值。
这可以在Scheme评估的环境模型框架中理解。
对(lambda (...) ...) 的每次调用都会创建一个新的 lambda 函数对象,隐含地与其定义环境配对,统称为 closure。
get-pairs 的每次调用都会创建自己的全新调用框架,并且从 that 创建的任何 lambdas 都将保留指向 (的副本) 的隐藏指针框架。
使用以下变体更容易看出这一点,它们的功能与问题中的变体完全相同:
(define (get-pairs1 alist proc)
(if (null? alist)
(proc '())
(get-pairs1
(cdr alist)
(let ((alist alist)) ; creates fresh new environment frame
(lambda (l)
(let ((num (car alist)))
(if (zero? (remainder num 2))
(proc (cons num l))
(proc l))))))))
(define (get-pairs2 alist proc)
(if (null? alist)
(proc '())
(get-pairs2
(cdr alist)
(let* ((alist alist)
(num (car alist))
(newproc
(if (zero? (remainder num 2))
(lambda (l) (proc (cons num l)))
(lambda (l) (proc l)))))
newproc))))
proc 没有“在最后进行评估”,procedure,即变量proc 的值被调用 在最后,但变量proc 的值是在每次调用时找到的。并且在每次调用时,该值都不同,即 new lambda 函数对象在每次单独调用 get-pairs 时重新创建。每次调用get-pairs时变量proc的值都不一样。
所以,对于示例调用(get-pairs2 '(1 2 3 4) display),最终proc的调用与
((lambda (l4) ; |
((lambda (l3) ; | |
((lambda (l2) ; | | |
((lambda (l1) ; | | | |
(display ; 1 2 3 4
l1)) ; | | | |
(cons 2 l2))) ; | | |
l3)) ; | |
(cons 4 l4))) ; |
'())
;; i.e.
;; l1 = cons 2 l2
;; l2 = l3
;; l3 = cons 4 l4
;; l4 = '()
也可以用伪代码写成
(((((display ∘ identity) ∘ {cons 2}) ∘ identity) ∘ {cons 4}) '())
; └───────1──────────┘
; └───────────────2───────────────┘
; └─────────────────────────3──────────────────┘
;└───────────────────────────────────4─────────────────────┘
;; 1: created on 1st invocation of `get-pairs2`
;; 2: created on 2nd invocation of `get-pairs2`
;; 3: created on 3rd invocation of `get-pairs2`
;; 4: created on the final 4th invocation of `get-pairs2`,
;; and then called with `'()` as the argument
其中{cons n} 表示部分应用的cons,即(lambda (l) (cons n l)),而identity 是(lambda (l) l)。
哦,∘ 代表函数组合,(f ∘ g) = (lambda (x) (f (g x)))。
另请参阅我的其他一些可能相关的答案,here 和 here。
逐步完成调用(get-pairs2 '(1 2 3 4)),基于let的重写模拟函数调用,我们得到(简化一点)
(get-pairs2 '(1 2 3 4) display)
=
(let ((alist '(1 2 3 4)) ; '(1 2 3 4)
(proc display))
(let* ((num (car alist)) ; 1
(newproc (lambda (l) (proc l))))
(let ((alist (cdr alist)) ; '(2 3 4)
(proc newproc))
(let* ((num (car alist)) ; 2
(newproc (lambda (l) (proc (cons num l)))))
(let ((alist (cdr alist)) ; '(3 4)
(proc newproc))
(let* ((num (car alist)) ; 3
(newproc (lambda (l) (proc l))))
(let ((alist (cdr alist)) ; '(4)
(proc newproc))
(let* ((num (car alist)) ; 4
(newproc (lambda (l) (proc (cons num l)))))
(let ((alist (cdr alist)) ; '()
(proc newproc))
(proc '()))))))))))
在 DrRacket 的代码编辑窗口中加载它并将鼠标悬停在各种标识符上是一个有趣的游戏,可让您查看每个标识符所指的内容。使用 Ctrl-R 运行此代码也会产生与原始函数调用相同的结果。
另一个“有趣”的练习是检查上面嵌套的let 表达式并通过为其添加唯一索引来手动重命名每个标识符(将proc 更改为proc1、proc2 等),这样每个名称变得独一无二。
好的,我会为你做的,特别是 DrRacket 有一个很好的“重命名标识符”功能,这使它更容易,更不容易出错。但也请尝试自己做。
(let ((alist '(1 2 3 4)) ; '(1 2 3 4)
(proc display))
(let* ((num (car alist)) ; 1
(newproc (lambda (l) (proc l))))
(let ((alist2 (cdr alist)) ; '(2 3 4)
(proc2 newproc))
(let* ((num2 (car alist2)) ; 2
(newproc2 (lambda (l) (proc2 (cons num2 l)))))
(let ((alist3 (cdr alist2)) ; '(3 4)
(proc3 newproc2))
(let* ((num3 (car alist3)) ; 3
(newproc3 (lambda (l) (proc3 l))))
(let ((alist4 (cdr alist3)) ; '(4)
(proc4 newproc3))
(let* ((num4 (car alist4)) ; 4
(newproc4 (lambda (l) (proc4 (cons num4 l)))))
(let ((alist5 (cdr alist4)) ; '()
(proc5 newproc4))
(proc5 '()))))))))))
所以你看,proc 不一样。其中有五个,每个都可能不同,每个都驻留在不同的嵌套环境框架中。
你可能会问,为什么是嵌套环境?毕竟get-pairs2是尾递归的,所以它不能那样做,可以在下一次调用时重用它的调用帧。
确实如此,但它仍然是与代码运行效率相关的实现细节,不会改变其含义(语义)。 语义上通过嵌套的let 重写,更容易看出代码的含义。
尽管如此,这是一个有效的观点,也是您困惑的潜在根源。我曾经也对这一点感到困惑。
这就是为什么我在这篇文章的开头写了“(the copy of) the environment frame”。即使尾递归调用可以——甚至可能必须,在 Scheme 的 TCO 保证下——在下一次调用中重新使用它自己的调用框架,新创建的闭包必须保持它自己的副本 , 以免引入语义不同的标识符的错误合并。
确实,环境扁平化和帧重用可以通过以下时间计算来描述:
;; re-use the tail-recursive call frame {alist proc}
(let ((alist '(1 2 3 4))
(proc display)
(num #f))
(set! num (car alist)) ; 1
(set! proc (let ((num num) (proc proc)) ; closure!
(lambda (l) (proc l))))
(set! alist (cdr alist)) ; (2 3 4)
(set! num (car alist)) ; 2
(set! proc (let ((num num) (proc proc)) ; closure!
(lambda (l) (proc (cons num l)))))
(set! alist (cdr alist)) ; (3 4)
(set! num (car alist)) ; 3
(set! proc (let ((num num) (proc proc)) ; closure!
(lambda (l) (proc l))))
(set! alist (cdr alist)) ; (4)
(set! num (car alist)) ; 4
(set! proc (let ((num num) (proc proc)) ; closure!
(lambda (l) (proc (cons num l)))))
(set! alist (cdr alist)) ; ()
(proc '()))
或者作为它可能实际编译为的定义,
(let ((alist '(1 2 3 4))
(proc display)
(num #f))
(let loop ()
(set! num (car alist))
(set! proc (let ((num num) (proc proc))
(if (zero? (remainder num 2))
(lambda (l) (proc (cons num l)))
(lambda (l) (proc l)))))
(set! alist (cdr alist))
(if (null? alist)
(proc '())
(loop))))
那么现在有多少procs? :)
(仍然是五个,否则它将无法工作......即有一个 binding 但在循环运行期间创建了五个 值,每个将前一个值包含在其中(或者,实际上,持有对它的引用);当最后一个 proc 值(它是一个函数)最终运行时,它会调用它“内部”的那个,然后那个调用“里面”的那个it,以此类推回到第一个proc,我们已经开始的display。)