【问题标题】:Can any case of using call/cc be rewritten equivalently without using it?任何使用 call/cc 的情况都可以在不使用的情况下等效地重写吗?
【发布时间】:2019-12-14 00:30:31
【问题描述】:

任何使用call/cc的情况都可以在不使用的情况下等效地重写吗?

例如

  1. (g (call/cc f))中,是f的目的是评估值 一些expression,以便g可以应用于值?

    (g (call/cc f)) 是否总是能够等效地重写 没有call/cc 例如(g expression)?

  2. ((call/cc f) arg) 中,f 的目的是评估 一些函数g的定义,所以函数g可以是 应用于arg的值?

    ((call/cc f) arg) 是否总是能够等效地重写 没有call/cc 例如(g arg)?

如果答案是肯定的,为什么我们需要使用call/cc? 我试图通过对比不使用它来了解使用call/cc 的目的。

【问题讨论】:

  • 请注意,2 是 1 的子情况,其中 g 定义为 (λ (h) (h arg))

标签: scheme lisp racket continuations callcc


【解决方案1】:

这里直接回答的关键是“图灵等价”的概念。也就是说,基本上所有常用的编程语言(C、Java、Scheme、Haskell、Lambda Calculus 等)都是等价的,因为对于其中一种语言的任何程序,每种语言都有相应的程序其他具有相同含义的语言。

不过,除此之外,其中一些等价物可能“很好”,而另一些可能真的很糟糕。这表明我们重新构建了这个问题:哪些功能可以以“不错”的方式重写为没有该功能的语言,哪些不能?

Matthias Felleisen 在他 1991 年的论文“On the Expressive Power of Programming Languages”(https://www.sciencedirect.com/science/article/pii/016764239190036W) 中对此进行了正式处理,该论文引入了宏可表达性的概念,指出某些功能可以用本地方式,有些需要全局重写。

【讨论】:

    【解决方案2】:

    您原来的问题的答案显然是肯定的。不管有没有call/cc,Scheme 都是图灵完备的,所以即使没有call/cc,你仍然可以计算任何可计算的东西。

    为什么“比使用 lambda 写等价表达式更方便”?

    Matthias Felleisen 的经典论文On the Expressive Power of Programming Languages 给出了这个问题的一个答案。几乎,要将带有call/cc 的程序重写为没有它的程序,您可能需要转换整个程序(全局转换)。这是为了对比其他一些只需要局部转换(即可以写成宏)来移除它们的构造。

    【讨论】:

    • 哈!我想你比我快 30 秒 :)。
    【解决方案3】:

    关键是:如果你的程序是用连续传递风格编写的,你不需要call/cc。如果没有,祝你好运。

    我全心推荐:

    丹尼尔·P·弗里德曼。 “延续的应用:特邀教程”。 1988 年编程语言原理 (POPL88)。 1988 年 1 月

    https://cs.indiana.edu/~dfried/appcont.pdf

    如果您喜欢阅读那篇论文,请查看: https://github.com/scheme-live/bibliography/blob/master/page6.md

    【讨论】:

      【解决方案4】:

      当然,任何用call/cc 编写的东西都可以不用它来编写,因为Scheme 中的所有东西 最终都是使用lambda 编写的。您使用 call/cc 是因为它比使用 lambda 编写等效表达式更方便。

      【讨论】:

      • 谢谢。为什么“比用 lambda 写等价表达式更方便”?
      • https://www.scheme.com/tspl3/examples.html#g171 上查看“使用引擎的多任务处理”的实现。尝试在没有call/cc 的情况下实现它,并让我们知道您的进展情况。
      • @Ben 在 CPS 中就像 (sqrt (+ (* a a) (* b b))) 变成 (*& a a (lambda (a2) (*& b b (lambda (b2) (+& a2 b2 (lambda (m) (sqrt& m continuation))))) 一样简单。这个简单的例子在 CPS 中已经很难理解了。想象一个困难的例子。
      【解决方案5】:

      这个问题有两种意义:一种无趣和一种有趣:

      无趣的。 有没有可以用call/cc 做的计算,而用没有它的语言做不到?

      不,不存在:call/cc 并没有使语言更强大:众所周知,只有 λ 和函数应用程序的语言等同于通用图灵机,因此没有(已知...)更强大的计算系统。

      但从编程语言设计的角度来看,这有点无趣:受内存和 c 的正常限制,几乎所有编程语言都等同于 UTM,但人们仍然更喜欢使用不涉及的语言如果可以的话,在纸带上打孔。

      有趣的。call/cc 是否使编程语言的某些理想功能更易于表达?

      答案是肯定的,确实如此。我只举几个例子。假设您想在您的语言中使用某种非本地退出功能,因此一些深度嵌套的程序可以说“我想退出这个地狱”,而不必爬回一些很棒的层职能。这对于call/cc 来说是微不足道的:继续过程转义过程。如果你想让它更好,你可以用一些语法包装它:

      (define-syntax with-escape
        (syntax-rules ()
          [(_ (e) form ...)
           (call/cc (λ (e) form ...))]))
      
      (with-escape (e)
        ... code in here, and can call e to escape, and return some values ...)
      

      如果没有call/cc,你能实现这个吗?嗯,是的,但不是不依赖其他一些特殊构造(比如 CL 中的 blockreturn-from),或者不以某种方式彻底改变语言。

      您可以在这样的基础上构建各种非本地转义。

      或者,假设您想要 GO TO(以下示例是 Racket):

      (define (test n)
        (define m 0)
        (define start (call/cc (λ (c) c)))
        (printf "here ~A~%" m)
        (set! m (+ m 1))
        (when (< m n)
          (start start)))
      

      或者,使用一些语法:

      (define-syntax-rule (label place)
        (define place (call/cc identity)))
      
      (define (go place)
        (place place))
      
      (define (horrid n)
        (define m 0)
        (label start)
        (printf "here ~A~%" m)
        (set! m (+ m 1))
        (when (< m n)
          (go start)))
      

      所以,好吧,这可能不是编程语言的理想特性。但是,好吧,Scheme 没有 GO TO 权限,但在这里却有。

      所以,是的,call/cc(尤其是与宏结合使用时)使编程语言的许多理想特性得以表达。其他语言有所有这些特殊用途的、有限的 hack,Scheme 有这个通用的东西,所有这些特殊用途的 hack 都可以从中构建。

      问题在于call/cc 并没有停止good 特殊用途的黑客攻击:您还可以利用它构建所有曾经破坏编程语言的可怕恐怖。 call/cc 就像接触了一位上古之神:如果你想要恐惧力量真的很方便,但你打电话时最好小心它带来的东西,因为它很可能是超越时空的无法形容的恐怖。

      【讨论】:

        【解决方案6】:

        call/cc 的一个简单使用是作为一种救助。例如。

        ;; (1 2) => (2 4)
        ;; #f if one element is not a number
        (define (double-numbers lst)
          (call/cc
           (lambda (exit)
             (let helper ((lst lst))
               (cond ((null? lst) '())
                     ((not (number? (car lst))) (exit #f))
                     (else (cons (* 2 (car lst)) (helper (cdr lst)))))))))
        

        所以要理解这一点。如果我们在做(double-numbers '(1 2 r)),结果是#f,但是助手已经做了(cons 1 (cons 2 (exit #f)))

        如果没有call/cc,我们会看到延续将是任何称为double-numbers 的东西,因为它实际上从它正常返回。这是一个没有call/cc的例子:

        ;; (1 2) => (2 4)
        ;; #f if one element is not a number
        (define (double-numbers lst)
          (define (helper& lst cont)
            (cond ((null? lst) (cont '()))
                  ((not (number? (car lst))) #f) ; bail out, not using cont
                  (else (helper& (cdr lst)
                                 (lambda (result)
                                   (cont (cons (* 2 (car lst)) result)))))))
          (helper& lst values)) ; values works as an identity procedure
        

        我想它会很快变得更难。例如。 my generator implementation。生成器依赖于访问延续来将生成器代码与其使用位置混合,但是如果没有call/cc,您将需要在生成器、生成的生成器和使用它的代码中执行 CPS。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-12-17
          • 2023-03-09
          • 1970-01-01
          • 2011-04-09
          • 2018-09-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多