65.9K
CodeProject 正在变化。 阅读更多。
Home

F# 14:递归

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2014 年 4 月 4 日

CPOL

5分钟阅读

viewsIcon

22847

F# 中的递归。

引言

我们将继续 F# 的探索之旅,这次我们将重点关注递归。我们已经在一些地方看到过递归,例如在讨论列表和模式匹配时。因此,有些内容对您来说应该会有点熟悉。

简单示例

让我们从最基本的示例开始,这与我们之前看到的示例类似。这个示例只是将输入列表中的所有元素相加。

let rec sumFunction theList =
    match theList with
    | []    -> 0
    | head::tail -> head + sumFunction tail

printfn "sumFunction [1..10] = %A" (sumFunction [1..10])

运行后会产生以下输出:

image

这个简短的代码片段有几个要点需要注意,例如:

  • 我们必须使用“rec”关键字才能使函数成为递归函数。如果没有使用“rec”关键字,编译器会发出错误。

image

  • 我们对空列表进行了模式匹配。
  • 我们使用“::”(cons)模式匹配,这样我们就可以匹配列表的头部和尾部。

现在让我们将注意力转向另一个示例,这次我们将使用众所周知的阶乘示例。下面是递归实现此功能的标准方法:

let rec factorial n = 
    if n = 0 then 1 else n * factorial (n-1)

这看起来不错,让我们来试试。尝试用输入运行它:

printfn "factorial System.Int32.MaxValue = %A" (factorial 10)

运行后确实一切正常,并给出了以下结果:

image

printfn "factorial System.Int32.MaxValue = %A" (factorial System.Int32.MaxValue)

那么现在会发生什么?让我们来看看……

现在我们看到了这个输出,这并不太好,实际上,用我以前老板的话来说,这简直是“一团糟”(请原谅我的脏话)。

image

那么为什么会发生这种情况?我们做错了什么?

好吧,如果我们看看实际的 Visual Studio IDE 并查看“CallStack”(调用堆栈)窗口,我们可以找到一些线索来解释为什么会发生这种情况。

image

因此,我们有大量的调用堆叠在堆栈上。这实际上是由于代码的结构方式。让我们再次检查代码:

let rec factorial n = 
    if n = 0 then 1 else n * factorial (n-1)

可以看到我们使用了 n,但我们还在等待递归调用“factorial”的结果,以便将它们相乘。所有这些调用都需要放置在某个地方,最终它们会被放在堆栈上,最终我们用完了空间,并抛出了 StackOverFlowException(这是完全正确的)。

您应该问自己的是,是否有更好的方法来编写代码来避免这种情况?

是的,有一种方法,它被称为“尾部递归”,它还使用一种称为“累加器模式”的技术。

尾递归

累加器模式”到底是什么意思?就其本身而言,它意味着我们将一个额外的值传递给函数,这是一个累加值,您可以在每次迭代调用中使用它。这个简单的更改意味着我们不再需要将东西压入堆栈来保存临时值,显然堆栈仍然与递归本身有关,但我们不必将不必要的值压入堆栈。因此,编译器能够优化代码。

好的,让我们看看新的/更好的代码(为了让我不必等太久就能看到结果,我选择了“5”这个较小的值,但请放心,如果您使用此技术处理更大的数字,就不会抛出 StackOverFlowException)。

let factorialProducingForLoop x =
    // Keep track of both x and an accumulator value (acc)
    let rec tailRecursiveFactorial x acc =
        if x <= 1 then 
            acc
        else 
            tailRecursiveFactorial (x - 1) (acc * x)
    tailRecursiveFactorial x 1

printfn "factorialProducingForLoop 5 = %A" (factorialProducingForLoop 5)

可以看到,我使用了一个非递归函数,该函数被调用,而该函数包含一个嵌套函数,该函数执行递归,并用初始累加器值和原始函数输入值调用。

这是此方法有效且正常工作的证明:

image

为了完全理解非尾部递归版本和更好的尾部递归版本之间的区别,让我们看看它们的反编译源代码(我使用 DotPeek 来反编译代码,但您可以使用任何您喜欢的工具)。

非尾部递归版本

这是未经优化的非尾部递归版本的反编译 C# 代码,可以看到这会严重依赖堆栈,这在下面显示的 Invoke() 方法中很明显,可以看到 Invoke() 方法被一次又一次地调用。

[Serializable]
  internal class factorial\u004015 : FSharpFunc<int, int>
  {
    internal factorial\u004015()
    {
    }

    public override int Invoke(int n)
    {
      if (n == 0)
        return 1;
      else
        return n * this.Invoke(n - 1);
    }
  }

尾部递归版本

这是优化后的尾部递归版本的反编译 C# 代码,可以看到它已被转换为一个简单的 for **循环,该循环**完全包含在对 Invoke() 方法的单次调用中。这让我们处于一个更好/更愉快的位置。

[Serializable]
  internal class tailRecursiveFactorial\u004021 : OptimizedClosures.FSharpFunc<int, int, int>
  {
    internal tailRecursiveFactorial\u004021()
    {
    }

    public override int Invoke(int x, int acc)
    {
      for (; x > 1; {
        int num;
        x = num;
      }
      )
      {
        num = x - 1;
        acc *= x;
      }
      return acc;
    }
  }

  [Serializable]
  internal class factorialProducingForLoop\u004020 : FSharpFunc<int, int>
  {
    internal factorialProducingForLoop\u004020()
    {
    }

    public override int Invoke(int x)
    {
      return FSharpFunc<int, int>.InvokeFast<int>(
            (FSharpFunc<int, FSharpFunc<int, int>>) 
                    new Program.tailRecursiveFactorial\u004021(), x, 1);
    }

我只想再提一点,如果您采用这个已经很棒(并且完全符合预期)的尾部递归代码

let factorialProducingForLoop x =
    // Keep track of both x and an accumulator value (acc)
    let rec tailRecursiveFactorial x acc =
        if x <= 1 then 
            acc
        else 
            tailRecursiveFactorial (x - 1) (acc * x)
    tailRecursiveFactorial x 1

这给了我们这些结果

image

但反而决定更喜欢这样写,我们**不**创建内部递归函数,而是选择两个独立的函数,并且我们更改了值和累加器的顺序

let rec tailRecursiveFactorial acc x =
    if x <= 1 then 
        acc
    else 
        tailRecursiveFactorial  (acc * x) (x - 1)

let factorialProducingWhileLoop data = tailRecursiveFactorial 1 data
    
printfn "factorialProducingWhileLoop 5 = %A" (factorialProducingWhileLoop 5)

image

可以看到我们得到了相同的结果,但让我们检查反编译的代码。

[Serializable]
  internal class tailRecursiveFactorial\u004032 : OptimizedClosures.FSharpFunc<int, int, int>
  {
    internal tailRecursiveFactorial\u004032()
    {
    }

    public override int Invoke(int acc, int x)
    {
      while (x > 1)
      {
        int num = acc * x;
        --x;
        acc = num;
      }
      return acc;
    }
  }

  [Serializable]
  internal class factorialProducingWhileLoop\u004037 : FSharpFunc<int, int>
  {
    public FSharpFunc<int, FSharpFunc<int, int>> tailRecursiveFactorial;

    internal factorialProducingWhileLoop\u004037
              (FSharpFunc<int, FSharpFunc<int, int>> tailRecursiveFactorial)
    {
      this.tailRecursiveFactorial = tailRecursiveFactorial;
    }

    public override int Invoke(int data)
    {
      return FSharpFunc<int, int>.InvokeFast<int>(this.tailRecursiveFactorial, 1, data);
    }
  }

可以看到这次,我们在 Invoke() 方法调用中得到了一个 while 循环。

我不知道这是否会为这篇文章增加任何价值,但我只是想看看如果我改变顺序并且没有嵌套函数,而是使用两个函数而不是一个带有内部函数的函数会发生什么。

我想我只是一个极客(并且为此感到自豪)。

回调

还有一种处理递归的方法,它使用基于回调的技术,但说实话,我发现它不太容易理解,而且对于现在来说有点棘手。如果您有兴趣,我相信快速谷歌一下就能找到您想要的东西。

所以最终,这篇关于一个相当复杂主题的帖子相当简短,但我没有太多想说的了。

© . All rights reserved.