【问题标题】:What is the definition of "natural recursion"?“自然递归”的定义是什么?
【发布时间】:2015-11-22 11:44:32
【问题描述】:

The Little Schemer 中的The Third Commandment 表示:

在构建列表时,先描述第一个典型元素,然后将其用于自然递归。

“自然递归”的确切定义是什么?我问的原因是因为我正在上丹尼尔弗里德曼的编程语言原则课程,并且以下代码不被认为是“自然递归”:

(define (plus x y)
    (if (zero? y) x
        (plus (add1 x) (sub1 y))))

但是,以下代码被认为是“自然递归的”:

(define (plus x y)
    (if (zero? y) x
        (add1 (plus x (sub1 y)))))

我更喜欢“非自然递归”代码,因为它是尾递归的。然而,这样的代码被认为是诅咒。当我问为什么我们不应该以尾递归形式编写函数时,助理教练简单地回答说:“你不要弄乱自然递归。”

把函数写成“自然递归”的形式有什么好处?

【问题讨论】:

  • 也许自然递归是一个递归过程。因此实际上不是关于过程递归,而是算法的性质。您的第一个示例是递归过程,但不是递归过程,因为空间是恒定的。这实际上是一个迭代过程。迭代过程可以使用显式数据结构而不是系统堆栈来实现递归过程。有时递归过程中的递归过程更容易阅读。
  • “这样的代码被视为诅咒”。是否有人明确告诉过你,或者这是你的印象?
  • 虽然我不确定它是否值得回答,但作者可能受到了"primitive recursion" from computability theory 的影响。关键在于接受“复合”参数(例如h(S(y)))的函数将由g(y,h(y)) 定义。将列表视为列表时,对应的对象将更类似于h(cons(x,y)) = g(x,h(y))。这不是很准确,但可能有一些影响。
  • 部分好处是原始递归函数总是停止。证明弗里德曼所说的“自然递归”函数终止会更容易,因为可以更容易地证明每个递归都朝着基本情况移动。
  • @AaditMShah 我完全同意效率。我的观点更多地是关于证明某事是正确实现的难易程度。当我们定义 reverse(x::xs) == append(reverse(xs),[x]) 时,我们会得到一个非常简单的归纳证明,只要我们知道 append 有效。我们可以立即看到 x 是最后一个元素,等等。累加器版本更有效,但证明它是正确可能有点困难(至少在入门级别)。我完全赞成编写更高效的代码;我只是在猜测作者为什么叫...

标签: recursion scheme lisp racket the-little-schemer


【解决方案1】:
(define (plus x y)
(if (zero? y) x
    (add1 (plus x (sub1 y)))))

y != 0 时,它必须记住一旦知道(plus x (sub1 y)) 的结果,它就必须计算add1。因此,当 y 达到零时,递归最深。现在回溯阶段开始并执行add1。这个过程可以使用trace观察。

我做了跟踪:

(require racket/trace)
(define (add1 x) ...)
(define (sub1 x) ...)
(define (plus x y) ...)
(trace plus)

(plus 2 3)

这是踪迹:

>(plus 2 3)
> (plus 2 2)
> >(plus 2 1)
> > (plus 2 0)  // Deepest point of recursion
< < 2           // Backtracking begins, performing add1 on the results
< <3
< 4
<5
5               // Result

不同的是,另一个版本没有回溯阶段。它会调用自己几次,但它是迭代的,因为它正在记住中间结果(作为参数传递)。因此,该过程不会消耗额外的空间。


有时实现尾递归过程比编写它的迭代等价物更容易或更优雅。但是出于某些目的,您不能/可能不会以递归方式实现它。

PS : 我有一堂课讲了一些关于垃圾收集算法的内容。这样的算法可能不是递归的,因为可能没有剩余空间,因此没有空间用于递归。我记得有一个算法叫“Deutsch-Schorr-Waite”,一开始真的很难理解。首先他实现了递归版本只是为了理解这个概念,然后他编写了迭代版本(因此必须手动记住他从哪里来),相信我递归版本更容易但不能在实践中使用......

【讨论】:

  • OP 已经知道第一个版本是尾递归形式,我认为您没有回答实际问题。
【解决方案2】:

自然递归与您正在处理的类型的“自然”递归定义有关。在这里,您正在处理自然数;因为“显然”一个自然数要么是零,要么是另一个自然数的后继,所以当你想构建一个自然数时,你自然会输出 0(add1 z) 为其他一些自然数 z 恰好是递归计算的.

老师可能希望你在递归类型定义和该类型值的递归处理之间建立联系。如果你尝试处理树,你就不会遇到数字问题或列表,因为您经常以“不自然的方式”使用自然数,因此,您可能会自然而然地反对使用教堂数字来思考。

你已经知道如何编写尾递归函数的事实在这种情况下是无关紧要的:这显然不是你的老师谈论尾调用优化的目标,至少现在 .

副讲师一开始并没有太大帮助(“搞乱自然递归”听起来像“不要问”),但他/她在您提供的快照中给出的详细解释更合适。

【讨论】:

    【解决方案3】:

    “自然”(或只是“结构”)递归是开始向学生教授递归的最佳方式。这是因为它有 Joshua Taylor 指出的美妙保证:保证终止[*]。学生们很难将他们的头脑围绕在这种程序上,将其作为“规则”可以为他们节省大量的头撞墙。

    当您选择离开结构递归领域时,您(程序员)承担了额外的责任,即确保您的程序在所有输入上都停止;这是另一件需要思考和证明的事情。

    在你的情况下,它有点微妙。您有 两个 参数,并且您正在对第二个参数进行结构递归调用。事实上,根据这一观察结果(程序在参数 2 上是结构递归的),我认为 您的 原始程序与非尾调用程序几乎一样合法,因为它继承了相同的收敛证明。问丹这件事;我很想听听他要说什么。

    [*] 准确地说,在这里你必须立法排除各种其他愚蠢的东西,比如调用其他不会终止的函数等。

    【讨论】:

    • 说实话,我还没见过丹。他目前在 ICFP,2 周后他会回来。因此,我只与副讲师互动。他们真的很聪明,和他们交谈很有趣。我以前从未对使用过 Scheme 如此兴奋。然而,我的编程风格与他们在课堂上教授的内容相冲突。我喜欢编写尾递归函数,但这是不允许的(目前)。我喜欢使用 named-let 表达式,但不鼓励这样做。另外,他们使用了诸如“自然递归”之类的模糊术语,这有点令人困惑。总的来说,这是我最喜欢的课程。 =')
    • 我向我的助理教练询问了自然递归的问题,我们使用它的原因确实是因为有停止的保证。它旨在教学生如何递归思考。他是这么说的:i.stack.imgur.com/QhBcl.png
    • @AaditMShah 非常感谢您添加回复!
    猜你喜欢
    • 2013-07-15
    • 1970-01-01
    • 1970-01-01
    • 2023-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    • 2017-09-22
    相关资源
    最近更新 更多