【问题标题】:why is tail recursion required in all implementations in scheme?为什么方案中的所有实现都需要尾递归?
【发布时间】:2013-02-18 21:04:13
【问题描述】:

尾递归更高效,因为它重用相同的堆栈帧而不是创建一个新的堆栈帧,但为什么方案中的所有内容都需要这样做?

【问题讨论】:

  • 因为你刚才所说的。等待。 “Scheme 中的所有内容”都不是尾递归的;因为 TR 在 Scheme 中被广泛使用,所以 Scheme 要求正确实现尾递归。而且,因为 call/cc 没有它是不可能的
  • 没有尾调用就不能调用/抄送?我不明白你为什么提出这个要求。
  • @JohnClements 我的意思是说“如果没有适当的 TR,就不可能使用它”。对call/cc 的调用应该回退调用堆栈;但如果没有 TR,对函数的调用会保留其调用堆栈。所以即使你允许它,也会有堆栈爆炸。这是我的想法。
  • @WillNess:哦...我认为您的意思是“对使用 call/cc 捕获的延续的调用应该倒回调用堆栈”...我同意,但是 tail-calling/tail-递归与此正交;您可以拥有一种非尾部调用语言,该语言仍然允许由使用延续引起的堆栈修改。事实上,Rhino 是一个开源的 JavaScript 实现,它是非尾调用的,但具有持续捕获和调用机制。
  • @JohnClements 很有趣,我不知道(是的,这 我的意思... :))所以在 Rhino 中对捕获的延续的调用是不同的然后是一个普通的电话,然后呢?我会再读一些关于这方面的内容,谢谢。 :)

标签: scheme tail-recursion


【解决方案1】:

Scheme 没有goto,所以所有循环最终都是使用尾递归完成的。如果没有适当的尾递归保证,就没有可靠的方法在 Scheme 中提供循环。


更新:我想解释一下“最终使用尾递归”的意思。让我们看看do 宏,因为@newacct 提到了它。例如:

(do ((i 1 (+ i 1)))
    ((> i 10))
  (display i)
  (newline))

正如我提到的,Scheme 没有goto,所以它必须从某个地方获得循环。它实际上宏扩展为(类似于):

(let loop ((i 1))
  (unless (> i 10)
    (display i)
    (newline)
    (loop (+ i 1))))

注意这里的loop 不是关键字或内置函数。这是一个命名函数,由命名let 创建,并在unless 表单的底部被调用(通过尾递归)。

确实,Scheme 中的所有标准循环形式都使用尾递归。没有办法摆脱它。


† 以下是名为 let(松散地说)的宏扩展为:

(letrec ((loop (lambda (i)
                 (unless (> i 10)
                   (display i)
                   (newline)
                   (loop (+ i 1))))))
  (loop 1))

‡ 严格来说,它宏扩展为:

((letrec ((loop (lambda (i)
                  (unless (> i 10)
                    (display i)
                    (newline)
                    (loop (+ i 1))))))
   loop)
 1)

【讨论】:

  • scheme中有do,但很少使用
  • @newacct do 不使用goto。它在命名let 上宏扩展为尾递归。
  • R5RS 说“Do 是一个迭代结构”。它也可以作为一流的实现,不一定作为宏。看起来,它和 let 一样是语言的一部分。而Scheme 有call/cc,它是goto 类固醇。事实上 this 可能是真正的原因 - 没有 TR 保证 call/cc 是无法使用的。
  • @newacct 命名为let 被认为是一种更Scheemly 的做事方式。 :)
  • @WillNess 从技术上讲,您可以do 实现为内置语法而不是宏,但我无法想象有任何实现想要这样做,因为: 1. 命名为let 严格来说比do 更通用(do 的所有实例都可以重写为命名let,但反之不行),以及 2. do 很少使用。因此,在实践中,你确实需要适当的尾递归来循环,除非你想为此目的滥用call/cc
【解决方案2】:

尾递归是语言规范的强制要求。引用 R6RS 的5.11 部分:

Scheme 的实现必须是适当的尾递归。在称为尾上下文的某些句法上下文中发生的过程调用是尾调用。如果 Scheme 实现支持无限数量的活动尾调用,则它是正确的尾递归。如果被调用的过程仍可能返回,则调用处于活动状态。请注意,这包括常规返回以及通过稍后调用的 call-with-current-continuation 捕获的延续返回。在没有捕获的延续的情况下,调用最多只能返回一次,而活动调用将是那些尚未返回的调用。正确尾递归的正式定义可以在 Clinger 的论文 [5] 中找到。识别来自 (rnrs base (6)) 库的结构中的尾调用的规则在第 11.20 节中描述。​​

这样做的实际原因是尾递归允许使用递归实现有效的循环。

【讨论】:

    【解决方案3】:

    没有 TR 保证 call/cc 将无法使用。循环本身可以通过 do which is a part of the language 实现。

    编辑:事实证明这是不正确的。请参阅问题中 John Clements 的 cmets。一种语言可以具有非 TR 函数调用机制,但具有用于调用延续的特殊单独机制。

    【讨论】:

    猜你喜欢
    • 2013-02-28
    • 2011-07-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-07
    • 1970-01-01
    相关资源
    最近更新 更多