简体   繁体   English

试图让我了解Haskell的递归?

[英]Trying to get my head around recursion in Haskell?

I have used many recursive functions now but still have trouble getting my head around how such a function exactly works (i'm familiar with the second line (ie | n==0 = 1 ) but am not so familiar with the final line (ie | n>0 = fac (n-1) * n )). 我现在已经使用了很多递归函数,但仍然无法理解这样一个函数是如何工作的(我熟悉第二行(即| n==0 = 1 )但是对最后一行不太熟悉(即| n>0 = fac (n-1) * n ))。

fac :: Int -> Int
fac n
    | n==0 = 1
    | n>0  = fac (n-1) * n

Recursive algorithms are very closely linked to mathematical induction . 递归算法数学归纳密切相关。 Perhaps studying one will help you better understand the other. 也许学习一个会帮助你更好地理解另一个。

You need to keep two key principles in mind when using recursion: 使用递归时,您需要记住两个关键原则:

  • Base Case 基本情况
  • Inductive Step 归纳步骤

The Inductive Step is often the most difficult piece, because it assumes that everything it relies upon has already been computed correctly. 归纳步骤通常是最难的部分,因为它假设它所依赖的一切都已经正确计算出来。 Making this leap of faith can be difficult (at least it took me a while to get the hang of it), but it is only because we've got preconditions on our functions; 实现信仰的这种飞跃可能很困难(至少我需要一段时间才能掌握它),但这只是因为我们的功能有先决条件 ; those preconditions (in this case, that n is a non-negative integer) must be specified so that the inductive step and base case are always true . 必须指定那些前提条件(在这种情况下, n是非负整数),以便归纳步和基本情况始终为真

The Base Case is also sometimes difficult: say, you know that the factorial N! 基础案例有时也很难:比如说,你知道阶乘N! is N * (N-1)! N * (N-1)! , but how exactly do you handle the first step on the ladder? ,但你究竟如何处理阶梯上的第一步? (In this case, it is easy, define 0! := 1 . This explicit definition provides you with a way to terminate the recursive application of your Inductive Step.) (在这种情况下,很容易,定义0! := 1这个显式定义为您提供了一种终止归纳步骤的递归应用的方法。)

You can see your type specification and guard patterns in this function are providing the preconditions that guarantee the Inductive Step can be used over and over again until it reaches the Base Case, n == 0 . 您可以看到此函数中的类型规范和保护模式提供了保证Inductive Step可以一次又一次地使用的前提条件,直到它到达基本情况, n == 0 If the preconditions can't be met, recursive application of the Inductive Step would fail to reach the Base Case, and your computation would never terminate. 如果无法满足前提条件,则归纳步骤的递归应用将无法到达基本情况,并且您的计算将永远不会终止。 (Well, it would when it runs out of memory. :) (好吧,它会耗尽内存。:)

One complicating factor, especially with functional programming languages, is the very strong desire to re-write all 'simple' recursive functions, as you have here, with variants that use Tail Calls or Tail Recursion. 一个复杂的因素,特别是函数式编程语言,非常强烈地希望重新编写所有“简单”的递归函数,就像你在这里一样,使用Tail Calls或Tail Recursion的变体。

Because this function calls itself, and then performs another operation on the result, you can build a call-chain like this: 因为此函数调用自身,然后对结果执行另一个操作,您可以构建如下所示的调用链:

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

That deep call stack takes up memory; 深度调用栈会占用内存; but a compiler that notices that a function doesn't change any state after making a recursive call can optimize away the recursive calls. 但是一个编译器注意到函数在进行递归调用后没有改变任何状态可以优化掉递归调用。 These kinds of functions typically pass along an accumulator argument. 这些类型的函数通常传递累加器参数。 A fellow stacker has a very nice example: Tail Recursion in Haskell 另一个堆栈器有一个非常好的例子: Haskell中的Tail Recursion

factorial 1 c = c
factorial k c = factorial (k-1) (c*k)

This very complicated change :) means that the previous call chain is turned into this: 这个非常复杂的变化:)意味着前一个调用链变成了这个:

fac 3 1       fac 2 3
  fac 2 3     fac 1 6
    fac 1 6   6

(The nesting is there just for show; the runtime system wouldn't actually store details of the execution on the stack.) (嵌套只用于show;运行时系统实际上不会在堆栈上存储执行的细节。)

This runs in constant memory, regardless of the value of n , and thus this optimization can convert 'impossible' algorithms into 'possible' algorithms. 无论n的值如何,它都在恒定的内存中运行,因此这种优化可以将“不可能”的算法转换为“可能的”算法。 You'll see this kind of technique used extensively in functional programming, much as you'd see char * frequently in C programming or yield frequently in Ruby programming. 你会看到这样的函数式编程广泛使用的技术,就像你会看到char *经常在C编程或yield在Ruby编程频繁。

When you write | condition = expression 当你写| condition = expression | condition = expression it introduces a guard . | condition = expression它引入了一个警卫 The guards are tried in order from top to bottom until a true condition is found, and the corresponding expression is the result of your function. 从上到下按顺序尝试防护,直到找到真实条件,并且相应的表达式是您的功能的结果。

This means that if n is zero, the result is 1 , otherwise if n > 0 the result is fac (n-1) * n . 这意味着如果n为零,则结果为1 ,否则如果n > 0则结果为fac (n-1) * n If n is negative you get an incomplete pattern match error. 如果n为负数,则会出现不完整的模式匹配错误。

Once you've determined which expression to use, it's just a matter of substituting in the recursive calls to see what's going on. 一旦确定要使用哪个表达式,只需在递归调用中替换即可查看正在发生的事情。

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

Especially for more complicated cases of recursion, the trick to save mental health is not to follow recursive calls, but just assume that they "do the right thing". 特别是对于更复杂的递归情况,保存心理健康的诀窍不是遵循递归调用,而是假设他们“做正确的事”。 Eg in your fac example, you want to compute fac n . 例如,在你的例子中,你想要计算fac n Imagine you already have the result fac (n-1) . 想象一下,你已经有了结果fac (n-1) Then it's trivial to calculate fac n : just multiply it by n. 然后计算fac n是微不足道的:只需乘以n即可。 But the magic of induction is that this reasonig actually works (as long as you provide a proper base case in order to terminate recursion). 但归纳的神奇之处在于这个推理实际上是有效的 (只要你提供一个适当的基本情况来终止递归)。 So eg for Fibonacci numbers, just look what the base case is, and assume that you are able to calculate the function for all numbers smaller then n: 因此,例如对于斐波纳契数,只需查看基本情况,并假设您能够计算小于n的所有数的函数:

fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

See? 看到? You want to calculate fib n . 你想计算fib n It's easy if you would know fib (n-1) and fib (n-2) . 如果你知道fib (n-1)fib (n-2)就很容易了。 But you can simply assume you are able to calculate them, and that the "deeper levels" of recursion do the "right thing". 但是你可以简单地假设你能够计算它们,并且递归的“更深层次”做“正确的事情”。 So just use them, it will work. 所以只要使用它们就可以了。

Note that there are much better ways to write this function, as currently many values are recalculated very often. 请注意,有更好的方法来编写此函数,因为目前很多值都经常重新计算。

BTW: The "best" way to write fac would be fac n = product [1..n] . BTW:编写fac的“最佳”方式是fac n = product [1..n]

whats throwing you? 什么扔你? maybe the guards (the | ) are confusing things. 也许守卫( | )令人困惑。

You can think of the guards loosely as a chain of ifs, or a switch statement (difference being only one can run, and it directly evaluates to a result. does NOt perform a series of tasks, and certainly no side-effects. just evaluates to a value) 您可以将警卫松散地视为ifs链或switch语句(差异只有一个可以运行,并且它直接评估结果。不会执行一系列任务,当然也没有副作用。只是评估一个值)

To pan imperative-like seudo-code.... 泛泛义式的seudo代码....

Fac n:
   if n == 0: return 1
   if n > 0: return n * (result of calling fac w/ n decreased by one)

The call tree by other poster looks like it could be helpful. 其他海报的调用树看起来可能会有所帮助。 do yourself a favor and really walk through it 帮自己一个忙,真正走过它

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM