避免在函数式编程中重新分配 - 不好的例子?

时间:2012-12-24 12:55:31

标签: scala functional-programming variable-assignment concurrent-programming

在很多网络文章中,函数式编程被表示为避免各种变量重新分配,因此仅推广“最终”变量,至少是为了更好地阅读。

它们中的大多数是带有计数器变量递增的不良循环的样本。 (如着名的i++x = x + 1。 这里有一篇鲍勃叔叔的文章说明:FP Episode 1

因此,这些文章表明,依靠可变变量经常导致副作用,特别是防止我们所谓的“参照透明度”,因此,更难以构建在多线程或更好的多处理器上运行的程序

我的问题是:众所周知,i++通常是一个线程LOCAL变量,因此即使进行并发处理也不会发生任何问题。

为什么选择一个像带有局部变量的循环这样的例子作为赋值的缺点,并且允许直接得出并发编程存在风险的结论?这两件事都与我严格无关。

为了更清楚,为什么不选择重新分配全局变量(或字段对象),这显然是并发编程的enemy,而不像Java那样过度使用所有的锁定模板。

我真的认为这个循环示例并不是将函数式编程的好处传递给命令式程序员的最好例证。

此外,它导致与“noob”函数程序员混淆,因为Scala例如在List中使用了很多while循环模式.scala类:

override def take(n: Int): List[A] = {
    val b = new ListBuffer[A]
    var i = 0
    var these = this
    while (!these.isEmpty && i < n) {  
      i += 1   // reassignment here
      b += these.head
      these = these.tail
    }
    if (these.isEmpty) this
    else b.toList
  } 

2 个答案:

答案 0 :(得分:7)

我认为Odersky自己说他们的目标是使API能够正常运行,但内部代码是特定实现的最佳选择。所以你可能不应该搜索Scala库内部的“好用Scala”或“FP的好例子”。

使用可变状态来保存索引(例如)也非常容易出错。因此,您应该将操作用于整个集合(filter / map / flatMap等),这样您就不必担心“索引越界”等问题。尽管如此,这些操作通常会导致大量的临时/中间集合被创建,因此它们会导致额外的垃圾收集。这通常对99%的程序无关紧要,但同样,这些程序在Scala库内部尽可能优化。

所以,是的,除了练习以“尽可能少的可变状态”实现“生存”之外的所有东西都是单线程程序的好习惯,因为错误的可能性较小,可测试性更强,可读性更强。

答案 1 :(得分:6)

在一个简单的循环中,没有问题 - 它不是永远并发问题,你可以跟踪变量。好吧,也许吧。

// Take the first n items that pass p
def takeGood(n: Int)(p: A => Boolean): List[A] = {
  val b = new ListBuffer[A]
  var these = this
  var i = 0
  while (!these.isEmpty && i < n) {
    i += 1
    if (p(these.head)) b += these.head
    these = these.tail
  }
  b.toList
}

嗯,除非这不起作用 - 我们在每个循环上增加了i,而不仅仅是我们采取的循环。

如果你使用递归,至少,你正在做的事情变得更加明显:

def takeGood[A](these: List[A], n: Int)(p: A => Boolean)(b: ListBuffer[A] = new ListBuffer[A]): List[A] = {
  if (these.isEmpty || n <= 0) b.toList
  else if (p(these.head)) takeGood(these.tail, n-1)(p)({ b += these.head; b })
  else takeGood(these.tail, n)(p)(b)
}

因此,即使没有并发性,使用函数式风格也会有优势:有时(尤其是循环)它使循环更加明确,从而减少出错的可能性。

并发带来了额外的好处,因为一致但过时通常比不一致死锁好得多。但这不是使用迭代器在while循环中显示的内容。