Recursion “的定义是什么?”;自然递归;?

Recursion “的定义是什么?”;自然递归;?,recursion,scheme,lisp,racket,the-little-schemer,Recursion,Scheme,Lisp,Racket,The Little Schemer,各国: 在构建列表时,请描述第一个典型元素,然后将其转换为自然递归 “自然递归”的确切定义是什么?我之所以提出这个问题,是因为我正在学习丹尼尔·弗里德曼(Daniel Friedman)的编程语言原理课程,以下代码不被认为是“自然递归的”: (定义(加上x和y) (如果(零?y)x (加(增一x)(增一y)) 但是,以下代码被视为“自然递归”: (定义(加上x和y) (如果(零?y)x (增补1(加上x(子1 y()()))) 我更喜欢“非自然递归”代码,因为它是尾部递归的。然而,这样的代码

各国:

在构建列表时,请描述第一个典型元素,然后将其转换为自然递归

“自然递归”的确切定义是什么?我之所以提出这个问题,是因为我正在学习丹尼尔·弗里德曼(Daniel Friedman)的编程语言原理课程,以下代码不被认为是“自然递归的”:

(定义(加上x和y)
(如果(零?y)x
(加(增一x)(增一y))
但是,以下代码被视为“自然递归”:

(定义(加上x和y)
(如果(零?y)x
(增补1(加上x(子1 y()())))
我更喜欢“非自然递归”代码,因为它是尾部递归的。然而,这样的代码被认为是令人憎恶的。当我问到为什么我们不应该用尾部递归的形式来编写函数时,副讲师简单地回答说:“你不能搞乱自然递归。”

以“自然递归”形式编写函数有什么好处

y!=0
必须记住,一旦
的结果(加上x(sub1 y))
已知,它必须在其上计算
add1
。因此,当y达到零时,递归处于最深处。现在回溯阶段开始执行
add1
。可以使用
跟踪
观察此过程

我跟踪了:

(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
>(加上23)
>(加2)
>>(加2 1)
>>(加2 0)//递归的最深点
<<2//开始回溯,对结果执行add1

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

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

事实上,您已经知道如何编写尾部递归函数在这种情况下是不相关的:您的老师显然不想讨论尾部调用优化,至少现在是这样

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

“自然”(或只是“结构”)递归是开始教学生递归的最佳方式。这是因为它有约书亚·泰勒(Joshua Taylor)指出的极好的保证:它保证终止[*]。学生们很难将自己的头脑集中在这类课程上,把它变成一条“规则”可以让他们避免大量的头痛

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

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


[*]准确地说,你必须立法排除所有其他愚蠢的东西,比如对其他不终止函数的调用等。

也许自然递归是一个递归过程。因此,实际上不是关于过程递归,而是关于算法的性质。第一个示例是递归过程,但不是递归过程,因为空间是常量。这实际上是一个迭代过程。迭代过程可以使用显式数据结构而不是系统堆栈实现递归过程。有时递归过程中的递归过程更容易阅读,“这样的代码被认为是令人讨厌的”。是否有人明确告诉过你,或者这是你的印象?虽然我不确定这是否值得回答,但作者可能受到了影响。关键在于,接受“复合”参数(例如,
h(S(y))
)的函数将由
g(y,h(y))
定义。当将列表视为列表时,对应项将更类似于
h(cons(x,y))=g(x,h(y))
。这并不十分精确,但可能有一定的影响。部分好处是原始递归函数总是停止。证明Friedman所谓的“自然递归”函数终止可能更容易,因为证明每个递归都朝着基本情况移动更容易。@AaditMShah我完全同意效率。我的观点更多的是关于证明某个东西是正确的实现是多么容易或困难。当我们定义
reverse(x::xs)==append(reverse(xs),[x])
时,只要我们知道append是有效的,我们就会得到一个非常简单的归纳证明。我们可以立即看到x是最后一个元素,等等。累加器版本更有效,但要证明它是正确的可能有点困难(至少在入门级)。我完全赞成编写更高效的代码;我只是在猜测为什么作者称…OP已经知道第一个版本是尾部递归形式,而我不知道
>(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