【问题标题】:How does call/cc work with the CPS transformation from "Lisp in Small Pieces"?call/cc 如何与“Lisp in Small Pieces”中的 CPS 转换一起工作?
【发布时间】:2020-01-06 18:54:55
【问题描述】:

Lisp in Small Pieces 一书展示了从 Scheme 到延续传递风格的转变(第 5.9.1 章,适用于那些可以阅读这本书的人)。转换表示 lambda 形式的延续,call/cc 应该等同于简单的(lambda (k f) (f k k))

我不明白这是如何工作的,因为函数的应用和延续之间没有区别。

这是一个从除应用程序之外的所有内容中剥离的转换版本(完整版本可在this gist 中找到):

(define (cps e)
  (if (pair? e)
      (case (car e)
        ; ...
        (else (cps-application e)))
      (lambda (k) (k `,e))))

(define (cps-application e)
  (lambda (k)
    ((cps-terms e)
     (lambda (t*)
       (let ((d (gensym)))
         `(,(car t*) (lambda (,d) ,(k d))
                     . ,(cdr t*)))))))

(define (cps-terms e*)
  (if (pair? e*)
      (lambda (k)
        ((cps (car e*))
         (lambda (a)
           ((cps-terms (cdr e*))
            (lambda (a*)
              (k (cons a a*)))))))
      (lambda (k) (k '()))))

现在考虑来自Wikipedia 的 CPS 示例:

(define (f return)
  (return 2)
  3)

上述转换会将函数体(return 2) 中的应用程序转换为(return (lambda (g13) ...) 2) 之类的东西。延续作为第一个参数传递,值2 作为第二个参数传递。如果return 是一个普通函数,那就没问题了。但是,return 应该是一个延续,它只接受一个参数。

我看不出这些部分是如何组合在一起的。转换如何将延续表示为 lambda 形式而不特别考虑它们的应用?

【问题讨论】:

    标签: scheme lisp continuation-passing


    【解决方案1】:

    我不明白这是如何工作的,因为函数的应用和延续之间没有区别。

    在没有 CPS 的情况下实现延续需要虚拟机级别的方法,例如使用“意大利面条堆栈”:在受垃圾回收影响的堆分配帧中分配词法变量。捕获一个延续就意味着获得一个环境指针,该指针指向意大利面条堆栈中的词汇框架。

    CPS 用闭包构建了一个 de facto 意大利面条堆栈。闭包将词法绑定捕获到具有无限生命周期的对象中。在 CPS 下,所有闭包都会捕获隐藏变量 kk 在意大利面条堆栈中充当父帧指针的角色;它将闭包链接在一起。

    因为整个程序一直是 CPS 转换的,所以到处都有一个 k 参数,它指向一个动态链接的封闭环境链,相当于一个事实上的 可以恢复执行的堆栈。

    难题的一个缺失部分是 CPS 依赖于尾调用。尾调用确保我们没有使用真正的堆栈;所有有趣的东西都在封闭的环境中。

    (但是,即使尾调用也不是严格要求的,正如 Henry Baker 的方法,体现在 Chicken Scheme 中告诉我们的那样。我们的 CPS 转换代码可以使用消耗堆栈但永远不会返回的真实调用。每隔一段时间,我们可以将可达的环境帧(以及所有的条件对象)从栈中移动到堆中,并回退栈指针。)

    现在考虑来自 Wikipedia 的 CPS 示例:

    啊,但这不是 CPS 示例;这是一个应用程序代码示例,它使用通过call/cc 以某种方式可用的延续。

    如果我们手动将其转换为 CPS,或者使用机械地执行此操作的编译器,它就会变成 CPS。

    然而,return 应该是一个延续,它只接受一个参数。

    因此,return 只接受一个参数,因为我们正在查看尚未经过 CPS 转换的应用程序源代码。

    应用程序级延续有一个参数。

    与所有函数一样,CPS 实现级别的延续将具有隐藏的 k 参数。

    k 参数类似于一段机器上下文,如堆栈或帧指针。当使用常规语言时,调用print("hello"),你不会问,为什么只有一个参数? print 不是必须接收堆栈指针才能知道参数在哪里吗?当然,当print 被编译时,编译后的代码有一种方法可以将上下文从一个函数传递到另一个函数,这对高级语言是不可见的。

    对于Scheme中的CPS,很容易混淆,因为源语言和目标语言都是Scheme。

    【讨论】:

    • 感谢您抽出宝贵时间解释这些细节,很抱歉我在表达我的问题要点方面做得很差。现在我意识到我对源语言/目标语言感到困惑,应该问一个完全不同的问题。
    猜你喜欢
    • 2012-05-08
    • 2019-12-13
    • 2018-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-17
    • 2017-08-01
    • 2011-10-23
    相关资源
    最近更新 更多