Lambda 连续传球式程序

Lambda 连续传球式程序,lambda,scheme,evaluation,sicp,continuation-passing,Lambda,Scheme,Evaluation,Sicp,Continuation Passing,我很难理解连续传递样式的过程如何“记住”以前函数调用中的值 例如,我有以下步骤,将从列表中筛选偶数值: (define (get-pairs alist proc) (if (null? alist) (proc '()) (get-pairs (cdr alist) (lambda (l) (let ((num (car alist))) (if (zero? (remainder num 2))

我很难理解连续传递样式的过程如何“记住”以前函数调用中的值

例如,我有以下步骤,将从列表中筛选偶数值:

(define (get-pairs alist proc)
  (if (null? alist)
      (proc '())
      (get-pairs
       (cdr alist)
       (lambda (l)
         (let ((num (car alist)))
           (if (zero? (remainder num 2))
               (proc (cons num l))
               (proc l)))))))
然后我称之为:

(get-pairs '(1 2)
           (lambda (n) (display n)))
要获得预期结果,
(2)

get pairs
将递归调用自身,直到其参数
alist
为空。最后一个函数调用是:
(get pairs'()proc)
<代码>程序将是以下程序:

(lambda (l)
         (let ((num (car alist)))
           (if (zero? (remainder num 2))
               (proc (cons num l))
               (proc l))))
在这个lambda主体中,
alist
proc
是前面的函数调用的参数:
(get pairs'(2)proc)
。我的问题是,如果只在最后计算
proc
,每个lambda过程如何“记住”过去函数调用的参数

或者是在每次调用
get pairs
时,作为参数传递给下一次调用的lambda的主体被“分析”,相应的
alist
proc
值已经被替换到其主体中?

TL;DR:由尾部调用优化函数创建的闭包必须捕获其定义环境(相关部分)的副本。或者,忽略TCO部分,将其视为常规递归函数,其中在递归函数执行期间创建的任何lambda函数都是闭包,捕获它所引用的变量的值


这可以在方案评估环境模型的框架内理解

(lambda(…)…)
的每次调用都会创建一个新的lambda函数对象,与它的定义环境隐式配对,称为闭包

每次调用
get pairs
都会创建自己的新调用帧,从中创建的任何lambda都将保留隐藏指针到该帧中(该帧的副本)

使用以下变量可以更容易地看到这一点,这些变量执行与问题中的变量完全相同的功能:

(定义(get-pairs1-alist过程)
(如果为空,则为空)
(proc'())
(get-pairs1)
(cdr列表)
(let((alist));创建新的环境框架
(λ(l)
(let((num(car)))
(如果(零)(余数数字2))
(程序(cons num l))
(议事程序(()(())(())))
(定义(get-pairs2-alist过程)
(如果为空,则为空)
(proc'())
(get-pairs2
(cdr列表)
(让*)
(数量(汽车)
(newproc)
(如果(零)(余数数字2))
(lambda(l)(proc(cons num l)))
(lambda(l)(proc l‘‘‘)’)
newproc)
proc
不是“在最末端进行评估”,在最末端调用作为变量
proc
的值的过程,但在每次调用时都会找到变量
proc
的值。并且在每次调用时,该值是不同的,即在每次单独调用
get pairs
时重新创建新的lambda函数对象。每次调用
get pairs
时,变量
proc
的值是不同的

因此,对于示例调用
(get-pairs2'(1234)display)
,最后的
过程的调用与

((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))
,而
标识
(lambda(l)l)

哦,还有
代表函数组合,
(f∘ g) =(λ(x)(f(gx)))

另请参阅我的其他一些可能相关的答案,以及


通过调用
(get-pairs2'(1 2 3 4))
一步一步地工作,使用基于
let
的重写来模拟函数调用,我们得到(简化了一点)

将其加载到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
re-writes更容易理解代码的含义

然而,这是一个有效的观点,也是你困惑的潜在根源。我曾经也被这一点弄糊涂了

这就是为什么我在这篇文章的开头写了“环境框架的副本”。即使尾部递归调用在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 '()))
或者作为
;; 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))))