在Scala中避免while循环有什么好处吗?

时间:2013-09-07 15:13:50

标签: scala

阅读专家撰写的Scala文档可以让人觉得尾递归优于while循环,即使后者更简洁明了。这是一个例子

object Helpers {
    implicit class IntWithTimes(val pip:Int) {
            // Recursive
        def times(f: => Unit):Unit = {
            @tailrec
            def loop(counter:Int):Unit = {
                if (counter >0) { f; loop(counter-1) }
            }
            loop(pip)
        }

            // Explicit loop
        def :@(f: => Unit) = {
            var lc = pip
            while (lc > 0) { f; lc -= 1 }
        }   
    }
}   

(很明显,专家根本没有解决循环问题,但在示例中他们选择以这种方式编写一个循环,就像本能一样,这就是我提出的问题:我应该开发一个类似的本能..)

while循环中唯一可能更好的方面是迭代变量应该是循环体的局部变量,并且变量的变异应该在固定的位置,但Scala选择不提供该语法。

清晰度是主观的,但问题是(尾部)递归样式是否提供了改进的性能?

3 个答案:

答案 0 :(得分:16)

我很确定,由于JVM的局限性,Scala编译器不会对所有可能的尾递归函数进行优化,所以对你的问题的答案很短(有时是错误的)强>性能是没有。

对你这个更普遍的问题(拥有优势)的长期答案更为人为。请注意,通过使用while,您实际上是:

  1. 创建一个包含计数器的新变量。
  2. 改变那个变量。
  3. 逐个错误和可变性的危险将确保从长远来看,您将引入具有while模式的错误。实际上,您的times功能可以很容易地实现为:

    def times(f: => Unit) = (1 to pip) foreach f
    

    这不仅更简单,更小,而且还避免了任何瞬态变量和可变性的产生。实际上,如果您调用的函数类型对结果很重要,那么while构造将开始变得更加难以阅读。请尝试使用whiles

    以外的任何内容来实施以下内容
    def replicate(l: List[Int])(times: Int) = l.flatMap(x => List.fill(times)(x))
    

    然后继续定义一个执行相同的尾递归函数。


    更新:

    我听到你说:“嘿!这是作弊!foreach既不是while也不是tail-rec电话”。真的吗?请查看Scala对foreach的{​​{1}}的定义:

    Lists

    如果您想了解有关Scala中递归的更多信息,请查看this blog post。进入函数式编程后,请发疯并阅读Rúnar的blog post。更多信息herehere

答案 1 :(得分:4)

这些专家是否说过性能是什么原因?我认为他们的理由更多地与表达性代码和函数式编程有关。你能引用他们的论点的例子吗?

递归解决方案能够比更多命令式替代方案更有效的一个有趣原因是它们经常在列表上运行,并且只使用头部和尾部操作。这些操作实际上比在更复杂的集合上的随机访问操作更快。

另一个原因是基于时间的解决方案可能效率较低,因为随着问题的复杂性增加,它们会变得非常难看......

(我不得不说,在这一点上,你的例子不是一个好的例子,因为你的循环都没有做任何有用的事情。你的递归循环特别不典型,因为它什么都不返回,这意味着你缺少一个专业关于递归函数的一点。功能位。递归函数很多比重复相同操作 n 次的另一种方式更多。)

虽然循环不返回值并且需要副作用来实现任何目的。它是一种控制结构,仅适用于非常简单的任务。这是因为循环的每次迭代都必须检查状态的所有以决定接下来要做什么。如果存在多个潜在的退出路径(或者复杂性必须在循环中的整个代码中分布,这可能是丑陋和混淆的),循环布尔表达式也可能必须变得非常复杂。

递归函数提供了更清晰实现的可能性。一个好的递归解决方案将一个复杂的问题分解为更简单的部分,然后将每个部分委托给另一个可以处理它的函数 - 诀窍是其他函数本身(或者可能是一个相互递归的函数,尽管很少看到)在Scala中 - 与各种Lisp方言不同,它很常见 - 因为尾部递归支持很差)。递归调用的函数在其参数中仅接收更简单的数据子集,仅接收相关的状态;它只返回更简单问题的解决方案。因此,与while循环相反,

  • 函数的每次迭代只需要处理问题的简单子集
  • 每次迭代只关心其输入,而不关心整体状态
  • 每个子任务中的成功由处理它的调用的返回值明确定义。
  • 来自不同子任务的状态不能被纠缠(因为它隐藏在每个递归函数调用中)。
  • 多个退出点(如果存在)更容易清晰表示。

鉴于这些优点,递归可以更容易地实现有效的解决方案。特别是如果将可维护性视为长期效率的重要因素。

我要去找一些要添加的代码的好例子。同时,此时我总是推荐The Little Schemer。我会继续讨论为什么,但这是两天内本网站上的第二个Scala递归问题,所以请查看我的previous answer

答案 2 :(得分:4)

通常,直接尾递归函数(即,总是直接调用自身并且不能被覆盖的函数)将始终由编译器优化为while循环。您可以使用@tailrec注释来验证编译器是否能够为特定函数执行此操作。

作为一般规则,任何尾递归函数都可以作为while循环重写(通常由编译器自动重写),反之亦然

以(尾部)递归样式编写函数的目的不是为了最大限度地提高性能甚至是简洁性,而是为了使代码的意图尽可能清晰,同时最大限度地减少引入错误的可能性(通过消除可变变量)这通常使得更难以跟踪功能的“输入”和“输出”是什么)。正确编写的递归函数包含一系列对终止条件的检查(使用级联if - else或模式匹配)和递归调用(仅在不递归的情况下复数)如果没有满足终止条件。

当存在多种不同的终止条件时,使用递归的好处最为显着。一系列if条件或模式通常比单个while条件更容易理解,其中包含一大堆(可能复杂且相互关联的)布尔表达式&&',特别是如果返回值需要根据满足哪种终止条件而不同。