scala中的递归非常必要吗?

时间:2013-09-05 21:03:59

标签: scala recursion functional-programming

在coursera scala教程中,大多数示例都使用自上而下的迭代。部分地,正如我所看到的,迭代用于避免for / while循环。我来自C ++,对此感到有些困惑。

是否为/ while循环选择了迭代?它在生产中是否实用?堆栈溢出的风险是什么?效率怎么样?自下而上动态编程怎么样(特别是当它们不是尾部重复时)?

另外,我应该使用更少的“if”条件,而是使用更多“case”和子类吗?

5 个答案:

答案 0 :(得分:20)

真正高质量的Scala将使用非常少的迭代并且只会稍微递归一次。在低级命令式语言中循环操作通常最好使用高阶组合器,尤其是map和flatmap,还有filter,zip,fold,foreach,reduce,collect,partition,scan,groupBy和a好几个人。迭代最好只在性能关键部分进行,而递归仅在高阶组合器不太适合的某些深度边缘情况下完成(通常不是尾递归,fwiw)。在生产系统中编写Scala的三年中,我使用迭代一次,递归两次,每天映射大约五次。

答案 1 :(得分:11)

嗯,其中有几个问题。

递归的必要性

  1. 递归不是必需的,但它有时可以提供非常优雅的解决方案。
  2. 如果解决方案是tail recursive且编译器支持tail call optimisation,那么解决方案甚至可以有效。
  3. 正如已经说得好,Scala有许多组合函数可以用来更有效地执行相同的任务。
  4. 一个典型的例子是编写一个函数来返回 nth Fibonacci number。这是一个天真的递归实现:

    def fib (n: Long): Long = n match {
      case 0 | 1 => n
      case _ => fib( n - 2) + fib( n - 1 )
    }
    

    现在,这是低效的(绝对不是尾递归),但它的结构与Fibonacci序列的关系非常明显。不过,我们可以正确地进行尾递归:

    def fib (n: Long): Long = {
      def fibloop(current: Long, next: => Long, iteration: Long): Long = {
        if (n == iteration) 
          current
        else
          fibloop(next, current + next, iteration + 1)
      }
      fibloop(0, 1, 0)
    }
    

    这本来可以写得更简洁,但它是一种有效的递归实现。也就是说,它并不像第一个那么漂亮,而且它的结构与原始问题的关联性也不那么明显。

    最后,从elsewhere on this site无耻地偷走了Luigi Plinge的基于流的实现:

    val fibs: Stream[Int] = 0 #:: fibs.scanLeft(1)(_ + _)
    

    非常简洁,高效,优雅(如果你理解流和懒惰的评价)非常富有表现力。事实上它也是递归的; #::是递归函数,但是在懒惰评估的上下文中运行。你当然必须能够递归地想出这种解决方案。

    与For / While循环相比的迭代

    我假设你的意思是传统的C风格 for ,在这里。

    递归解决方案通常优于循环,因为C / C ++ / Java风格的while循环不返回值并且需要副作用来实现任何内容(对于C-Style也是如此) for 和Java风格的 foreach )。坦率地说,我经常希望Scala从来没有实现(或者已经将它实现为类似于Scheme的名字let的语法糖),因为它允许经过专业训练的Java开发人员继续做他们总是如此。有 的情况下,带有副作用的循环,这是给你的东西,是一种更具表现力的方式来实现某些东西,但我宁愿Java固定的开发者被迫稍微努力一点(例如滥用进行理解)。

    简单地说,传统的 使得笨重的命令式编码变得非常简单。如果你不关心这个,你为什么要使用Scala?

    Stackoverflow的效率和风险

    尾部优化消除了stackoverflow的风险。重写递归解决方案以使其适当地尾递归可能使它们非常难看(特别是在JVM上运行的任何语言中)。

    递归解决方案可以比更迫切的解决方案更有效,有时令人惊讶的是。一个原因是他们经常在列表上运行,只涉及 head tail 访问。列表上的头尾操作实际上比更结构化集合上的随机访问操作更快。

    动态编程

    一个好的递归算法通常会将一个复杂的问题简化为一小组更简单的问题,选择一个来解决并将其余的委托给另一个函数(通常是对自身的递归调用)。现在,对我而言,这听起来非常适合动态编程。当然,如果我正在尝试递归方法解决问题,我通常会从一个天真的解决方案开始,我知道它可以解决每个案例,看看它失败的地方,将该模式添加到解决方案并迭代成功。

    The Little Schemer有许多这种迭代方法用于递归编程的例子,特别是因为它重新使用早期的解决方案作为后续更复杂的子组件。我想说它是动态编程方法的缩影。 (它也是有史以来最好的软件教育书籍之一)。我可以推荐它,尤其是因为它同时教你Scheme。如果你真的不想学习Scheme(为什么?为什么不呢?),它已经适应了其他一些languages

    如果与匹配

    如果表达式,在Scala中,返回值(这非常有用,为什么Scala不需要三元运算符)。没有理由避免简单的

    if (something)
      // do something
    else
      // do something else
    

    表达式。 匹配而不是简单的 if ... else 的主要原因是使用case语句的强大功能从复杂对象中提取信息。 Here就是一个例子。

    另一方面,如果......其他如果......其他如果......其他是一种可怕的模式

    1. 即使最终其他到位,也没有简单的方法可以确定您是否正确涵盖了所有可能性。
    2. 无意中嵌套如果表达式难以发现
    3. 将不相关的条件联系在一起(意外或通过骨头设计)太容易了
    4. 无论您在哪里找到 else if ,都要寻找替代方案。 匹配是一个很好的起点。

答案 2 :(得分:5)

我假设,因为你在标题中说“递归”,你的意思也是“递归”,而不是“迭代”(不能选择“over for / while循环”,因为那些是迭代:D)。

您可能有兴趣阅读Effective Scala,尤其是有关控制结构的部分,这部分应该主要回答您的问题。简而言之:

递归并不比迭代“更好”。通常,为给定问题编写递归算法更容易,然后编写迭代算法(当然,有些情况则相反)。当“尾调用优化”可以应用于问题时,编译器实际上将其转换为迭代算法,从而使得StackOverflow不可能发生,并且不会对性能产生影响。您也可以在Effective Scala中阅读有关尾部调用优化的内容。

您的问题的主要问题是非常广泛。有很多资源可用于函数式编程,惯用scala,动态编程等,而Stack Overflow上没有任何答案可以涵盖所有这些主题。一段时间漫游互联网可能是一个好主意,然后带回更具体的问题:)

答案 3 :(得分:2)

递归的一个主要好处是它可以让你创建没有变异的解决方案。对于以下示例,您必须计算List的所有元素的总和。

解决此问题的众多方法之一如下。这个问题的必要解决方案使用for循环,如下所示:

    scala> var total = 0
    scala> for(f <- List(1,2,3)) { total += f }

递归解决方案如下所示:

   def total(xs: List[Int]): Int = xs match {
      case Nil => 0
      case x :: ys => x + total(ys)
}

不同之处在于,递归解决方案不会使用任何可变的临时变量,而是让您将问题分解为更小的部分。因为函数式编程完全是关于编写无副作用的程序,所以总是鼓励使用递归vs循环(使用变异变量)。

头递归是一种传统的递归方式,首先执行递归调用,然后从递归函数中获取返回值并计算结果。

通常,当您调用函数时,会将条目添加到当前正在运行的线程的调用堆栈中。缺点是调用堆栈具有定义的大小,因此很快就会出现StackOverflowError异常。这就是为什么Java更喜欢迭代而不是递归。由于Scala在JVM上运行,因此Scala也遇到了这个问题。但是从Scala 2.8.1开始,Scala通过尾部调用优化来消除这种限制。你可以在Scala中进行尾递归。

回顾递归是函数式编程中的首选方法,以避免使用变异,其次Scala支持尾递归,因此您不会进入Java中的StackOverFlow异常。

希望这有帮助。

答案 4 :(得分:0)

至于堆栈溢出,很多时候你可以通过消除尾部调用来消除它。

scala和其他函数范例避免for / while循环的原因是它们高度依赖于状态和时间。这使得在正式和精确的庄园中推理复杂的“循环”变得更加困难。