【问题标题】:F# tail recursion and why not write a while loop?F# 尾递归,为什么不写一个 while 循环?
【发布时间】:2011-08-20 09:39:53
【问题描述】:

我正在学习 F#(函数式编程的新手,虽然多年来一直使用 C# 的函数式方面,但让我们面对现实吧,这是非常不同的),我读过的一件事是 F# 编译器识别尾递归并将其编译成一个while循环(见http://thevalerios.net/matt/2009/01/recursion-in-f-and-the-tail-recursion-police/)。

我不明白为什么你会写一个递归函数而不是一个while循环,如果它无论如何都会变成这样的话。特别是考虑到您需要做一些额外的工作来使您的函数递归。

我有一种感觉,有人可能会说 while 循环不是特别实用,并且您想执行所有功能等等,因此您使用递归,但是为什么编译器将其转换为 while 循环就足够了?

谁能给我解释一下?

【问题讨论】:

标签: f# tail-recursion


【解决方案1】:

递归函数通常是处理某些数据结构(例如树和 F# 列表)的最自然方式。如果编译器出于性能原因想要将我自然、直观的代码转换为笨拙的 while 循环,那很好,但我为什么要自己编写呢?

此外,Brian 的 answer to a related question 与此处相关。高阶函数通常可以替换代码中的循环和递归函数。

【讨论】:

  • 唯一的障碍是“使用某些数据结构的最自然的方式”通常不是尾递归的。因此,我们只能自己将其转换为可读性较差的东西。不过还是比while循环好。 :-)
  • @petebu - 这就是我提出 Brian 的帖子的原因。循环和递归函数都不是没有缺点的,这就是为什么 mapfold 可以拥有这么好的原因。
【解决方案2】:

F# 执行尾优化的事实只是一个实现细节,它允许您以与 while 循环相同的效率(并且不用担心堆栈溢出)使用尾递归。但这只是 - 一个实现细节 - 从表面上看,您的算法仍然是递归的,并且是以这种方式构造的,对于许多算法来说,这是表示它的最合乎逻辑、最实用的方式。

这同样适用于 F# 中的一些列表处理内部结构 - 内部突变用于更有效地实现列表操作,但这一事实对程序员是隐藏的。

归根结底是语言如何让您描述和实现您的算法,而不是在底层使用什么机制来实现它。

【讨论】:

  • 我不认为“尾部优化只是一个实现细节”。垃圾收集只是一个实现细节吗?如果您关闭其中任何一个,它不仅会使某些程序运行速度变慢,它们还会因堆栈溢出或内存不足异常而崩溃。知道某些程序只会消耗恒定数量的堆栈空间(参见堆)非常有用。我希望 F# 规范对此做出一些保证。许多程序假设存在尾部调用消除(甚至更多假设垃圾收集),否则会中断。
  • @petebu:同意——我的意思是如何 F#执行尾部优化(即通过将其变成一个while循环)并不重要,事实上它确实当然是语言规范的一部分,因此某些程序需要。
  • 另外值得注意的是,F# 编译器不会总是将尾递归函数转换为 while 循环。根据具体情况,它可能会这样做,也可能会发出.tail IL 指令。
  • 你有没有关于规范在哪里保证尾调用的参考?我试图找到它(通过搜索“tail”)但找不到。
  • @petebu:显然那句话是错误的,这种优化不是语言的一部分,而是 F# 编译器的一个属性,我的错 - 应该更仔细地阅读你的第一条评论 ;-)
【解决方案3】:

您可以对编译器执行的任何转换使用相同的参数。例如,当您使用 C# 时,您是否曾经使用过 lambda 表达式或匿名委托?如果编译器只是要将它们转换为类和(非匿名)委托,那么为什么不自己使用这些结构呢?同样,您是否曾经使用过迭代器块?如果编译器只是要将它们转换为显式实现IEnumerable<T> 的状态机,那么为什么不自己编写该代码呢?或者,如果 C# 编译器无论如何都只是要发出 IL,那么为什么要首先编写 C# 而不是 IL?以此类推。

对所有这些问题的一个明显答案是,我们希望编写能够让我们清楚地表达自己的代码。同样,有许多算法是自然递归的,因此编写递归函数通常会导致这些算法的清晰表达。特别是,在许多情况下,递归算法的终止可能比 while 循环更容易推理(例如,是否有明确的基本情况,每次递归调用是否会使问题“更小”?)。

但是,由于我们正在编写代码而不是数学论文,因此拥有满足某些实际性能标准(例如能够处理大量输入而不会溢出堆栈的能力)的软件也很好。因此,将尾递归转换为等效的 while 循环这一事实对于能够使用算法的递归公式至关重要。

【讨论】:

  • 那么也许是因为我仍然对 while 循环感到更舒服,所以我问了这个问题......
  • @statichippo - 是的,如果您发现 while 循环比递归更容易推理,那么您的问题是非常合理的。但是,随着对函数式风格的更多了解,您可能会开始看到更适合递归的情况。
  • @statichippo - 当然你会更喜欢写你知道的东西,但考虑一下。 在我开始编写代码之前,我非常习惯于不编写代码。
  • +1 关于推理的要点。虽然我会声称它更容易推理(部分)正确性以及终止。这一点确实需要更频繁地强调 - 程序员一直在证明他们程序的属性(通常在他们的脑海中非正式地)。关于函数式程序的推理(使用归纳法)是非常自然的。不是关于命令式程序的推理。
【解决方案4】:

while 循环本质上是必不可少的。大多数时候,当使用while 循环时,你会发现自己在编写这样的代码:

let mutable x = ...
...
while someCond do
    ...
    x <- ...

这种模式在 C、C++ 或 C# 等命令式语言中很常见,但在函数式语言中并不常见。

正如其他发帖者所说,一些数据结构,更准确地说是递归数据结构,适合递归处理。由于迄今为止函数式语言中最常见的数据结构是单链表,因此使用列表和递归函数解决问题是一种常见的做法。

另一个支持递归解决方案的论点是递归和归纳之间的紧密关系。使用递归解决方案可以让程序员以归纳方式思考问题,这可以说有助于解决问题。

再次,正如其他海报所说,编译器优化尾递归函数(显然,并非所有函数都可以从尾调用优化中受益)这一事实是一个实现细节,它可以让您的递归算法在恒定空间中运行。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-20
    • 2013-09-12
    • 2011-07-13
    • 1970-01-01
    • 1970-01-01
    • 2013-08-12
    • 2018-06-24
    相关资源
    最近更新 更多