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

Mik*_*fey 18 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 }
        }   
    }
}   
Run Code Online (Sandbox Code Playgroud)

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

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

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

Hug*_*ira 18

我很确定,由于JVM的局限性,Scala编译器并不是每个潜在的尾递归函数都会被优化掉,因此对性能问题的简短(有时是错误的)答案是否定的.

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

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

逐个错误和可变性的危险将确保从长远来看,您将引入带有while模式的错误.实际上,您的times功能可以很容易地实现为:

def times(f: => Unit) = (1 to pip) foreach f
Run Code Online (Sandbox Code Playgroud)

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

def replicate(l: List[Int])(times: Int) = l.flatMap(x => List.fill(times)(x))
Run Code Online (Sandbox Code Playgroud)

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


更新:

我听到你说:"嘿!这是作弊!foreach既不是while也不是tail-rec电话".真的吗?看看到Scala的定义foreachLists:

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

如果您想了解有关Scala中递归的更多信息,请查看此博客文章.进入函数式编程后,请疯狂阅读Rúnar的博客文章.更多信息在这里这里.

  • Scala优化*直接尾递归*.但是,并不总是很容易看出递归调用*真的*是否直接用于同一个方法.特别是,即使*if*一个方法看似自称,它实际上可能会调用自身的重写或重载版本,从而使递归不再直接.如果有疑问,`@tailrec`. (3认同)
  • 我不得不承认我对函数式编程有点新意,但是如果减少一个计数器是一个"危险",那里有很多不好的代码,我想我会隐藏在我的房间里.:-) (2认同)

reg*_*ert 5

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

通常,可以将任何尾部递归函数(通常由编译器自动)重写为while循环,反之亦然

以(尾)递归样式编写函数的目的不是为了最大化性能甚至简明扼要,而是使代码的意图尽可能清晰,同时最小化引入错误的机会(通过消除可变变量,通常这是可变的)使其更难以跟踪函数的“输入”和“输出”是什么)。正确编写的递归函数包含一系列检查终止条件(使用级联if- else或模式匹配)的检查,如果没有满足任何终止条件,则进行递归调用(仅在没有尾递归的情况下为复数)。

当存在几种可能的终止条件时,使用递归的好处最为明显。通常,一系列if条件或模式比将while一堆(可能是复杂且相互关联的)布尔表达式&&组合在一起的单个条件更容易理解,尤其是如果返回值需要根据哪个终止条件而有所不同时,更是如此遇见。