试图了解Haskell中的递归?

试图了解Haskell中的递归?,haskell,recursion,factorial,Haskell,Recursion,Factorial,我现在已经使用了许多递归函数,但仍然难以理解这样一个函数到底是如何工作的(我熟悉第二行(即|n==0=1),但不太熟悉最后一行(即|n>0=fac(n-1)*n) 当您编写| condition=expression时,它会引入一个保护。按照从上到下的顺序尝试防护,直到找到真正的条件,并且相应的表达式是函数的结果 这意味着如果n为零,则结果为1,否则如果n>0则结果为fac(n-1)*n。如果n为负值,则会出现不完整的模式匹配错误 一旦确定了要使用哪个表达式,就只需在递归调用中进行替换,看看发生

我现在已经使用了许多递归函数,但仍然难以理解这样一个函数到底是如何工作的(我熟悉第二行(即
|n==0=1
),但不太熟悉最后一行(即
|n>0=fac(n-1)*n


当您编写
| condition=expression
时,它会引入一个保护。按照从上到下的顺序尝试防护,直到找到真正的条件,并且相应的表达式是函数的结果

这意味着如果
n
为零,则结果为
1
,否则如果
n>0
则结果为
fac(n-1)*n
。如果
n
为负值,则会出现不完整的模式匹配错误

一旦确定了要使用哪个表达式,就只需在递归调用中进行替换,看看发生了什么

fac 4
(fac 3) * 4
((fac 2) * 3) * 4
(((fac 1) * 2) * 3) * 4
((((fac 0) * 1) * 2) * 3) * 4
(((1 * 1) * 2) * 3) * 4
((1 * 2) * 3) * 4
(2 * 3) * 4
6 * 4
24

你怎么了?也许警卫(代码)把事情弄糊涂了

您可以将防护松散地视为ifs链或switch语句(区别在于只有一条语句可以运行,它直接计算结果。不执行一系列任务,当然也没有副作用。只计算一个值)

要像seudo代码一样平移命令行

Fac n:
   if n == 0: return 1
   if n > 0: return n * (result of calling fac w/ n decreased by one)
其他海报上的呼叫树看起来可能会有所帮助。帮你自己一个忙,真正地走过去

与你的关系非常密切。也许研究其中一个会帮助你更好地理解另一个

在使用递归时,需要记住两个关键原则:

  • 基本情况
  • 感应步
归纳步骤通常是最困难的部分,因为它假设它所依赖的一切都已经正确计算过了。实现这一信念的飞跃可能很困难(至少我花了一段时间才掌握窍门),但这只是因为我们的功能有先决条件;必须指定这些前提条件(在本例中,
n
为非负整数),以便归纳步长和基本情况始终为真

基本情况有时也很困难:比如,你知道阶乘
N
N*(N-1),但您如何准确处理阶梯上的第一步?(在这种情况下,很容易定义
0!:=1
。此显式定义为您提供了终止归纳步骤递归应用的方法。)

您可以看到此函数中的类型规范和保护模式提供了先决条件,以确保归纳步骤可以反复使用,直到达到基本情况,
n==0
。如果前提条件不能满足,递归应用归纳步骤将无法达到基本情况,您的计算将永远不会终止。(好吧,当它的内存不足时,它会这样做。:)

一个复杂的因素,特别是对于函数式编程语言,是非常强烈的愿望,即使用使用或尾部递归的变体重新编写所有“简单”递归函数,正如您在这里看到的那样

由于此函数调用自身,然后对结果执行另一个操作,因此可以构建如下调用链:

fac 3        3 * fac 2
  fac 2      2 * fac 1
    fac 1    1 * fac 0
      fac 0  1
    fac 1    1
  fac 2      2
fac 3        6
深度调用堆栈占用内存;但是,如果编译器注意到函数在进行递归调用后不会改变任何状态,则可以优化递归调用。这类函数通常传递累加器参数。一位同事有一个很好的例子:

这一非常复杂的更改:)意味着以前的调用链将变成:

fac 3 1       fac 2 3
  fac 2 3     fac 1 6
    fac 1 6   6
(嵌套只是为了展示;运行时系统实际上不会在堆栈上存储执行的详细信息。)


无论
n
的值是多少,它都在恒定内存中运行,因此这种优化可以将“不可能的”算法转换为“可能的”算法。你会看到这种技术在函数式编程中被广泛使用,就像你在C编程中经常看到的
char*
或者在Ruby编程中经常看到的
yield

特别是对于更复杂的递归情况,保存心理健康的诀窍是而不是遵循递归调用,但只要假设他们“做了正确的事情”。例如,在fac示例中,您希望计算
fac n
。假设您已经有了结果fac(n-1)
。那么计算fac n就很简单了:只需将它乘以n。但是归纳法的神奇之处在于,这个推理实际上是有效的(只要你提供一个合适的基本情况来终止递归)。例如,对于斐波那契数,只要看看基本情况是什么,并假设您能够计算所有小于n的数的函数:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
看到了吗?您要计算
fib n
。如果你知道
fib(n-1)
fib(n-2)
,这很容易。但是您可以简单地假设您能够计算它们,并且递归的“更深层次”做了“正确的事情”。所以只要使用它们,它就会起作用

请注意,有更好的方法来编写此函数,因为当前许多值经常被重新计算


顺便说一句:写
fac
的“最佳”方法是
fac n=product[1..n]

可能重复的精彩解释。这个问题很简单,但我想确保我理解为什么递归的情况是归纳的。您是否介意分享一些见解,让我们明白这一点?关于参数性质和输入到基本模式的结果值的假设是否证明递归情况是归纳的?@DavidShaked,递归情况通常在进行递归调用时以某种方式尝试“减少”参数——使用阶乘,通过减法,但这并不是唯一的选择——目标是最终进行不需要再递归的调用(基本情况)
fac 3 1       fac 2 3
  fac 2 3     fac 1 6
    fac 1 6   6
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)