【问题标题】:Is there any advantage to avoiding while loops in Scala?在Scala中避免while循环有什么好处吗?
【发布时间】:2013-09-07 15:13:50
【问题描述】:

阅读专家编写的 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 选择不提供这种语法。

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

【问题讨论】:

    标签: scala


    【解决方案1】:

    我很确定,由于 JVM 的限制,并不是每个潜在的尾递归函数都会被 Scala 编译器优化掉,所以对你的问题的简短(有时是错误的)回答 性能是没有。

    对您的更一般的问题(具有优势)的长答案有点做作。请注意,通过使用while,您实际上是:

    1. 创建一个包含计数器的新变量。
    2. 改变那个变量。

    一个接一个的错误和可变性的危险将确保从长远来看,您将引入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 的定义Lists

      def foreach[B](f: A => B) {
        var these = this
        while (!these.isEmpty) {
          f(these.head)
          these = these.tail
        }
      }
    

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

    【讨论】:

    • Scala 优化了直接尾递归。然而,并不总是很容易看出递归调用是否真的直接指向同一个方法。特别是,即使如果一个方法看似调用自己,它实际上也可能调用自己的重写或重载版本,从而使递归不再直接。如有疑问,@tailrec.
    • 得承认我对函数式编程有点陌生,但是如果递减计数器是一种“危险”,那么那里有很多糟糕的代码,我想我会躲在我的房间里。 :-)
    • 虽然对于范围的理解看起来很像一个不太通用的 java "for" 循环,但它们有一个问题,即在调试期间(至少使用 Intellij IDEA 插件)单步不进入正文的“for”(你必须在那里放一个断点),并且对外部类中的变量的引用不是“干净的”。当然,这些结构只是递增索引上循环的错觉,我想知道为什么 Scala 不为这种常用结构提供语法,即使它的使用可能比其他语言更受限制?
    • @MikeHanafey Scala 允许您编写 for (x <- 1 to 10) { do stuff with x }。你能简单到什么程度?它不需要特殊的语法。
    【解决方案2】:

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

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

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

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

    【讨论】:

    • 感谢您最清楚地回答为什么实际上要关注尾递归函数。
    【解决方案3】:

    这些专家是否说性能是原因?我打赌他们的原因更多地与表达性代码和函数式编程有关。你能举出他们论点的例子吗?

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

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

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

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

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

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

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

    我要去寻找一些很好的代码示例来添加。同时,在这一点上我总是推荐The Little Schemer。我会继续解释为什么,但这是两天内该网站上的第二个 Scala 递归问题,所以请查看我的 previous answer

    【讨论】:

      猜你喜欢
      • 2014-01-24
      • 1970-01-01
      • 2013-08-29
      • 1970-01-01
      • 1970-01-01
      • 2014-03-30
      • 1970-01-01
      • 1970-01-01
      • 2012-08-20
      相关资源
      最近更新 更多