避免函数式编程中的重新赋值——坏例子



在许多web文章中,函数式编程都被描述为避免各种类型的变量重新赋值,因此只提倡"最终"变量,尤其是为了更好地阅读。

它们中的大多数都是带有计数器变量递增的穷循环的样本。(比如著名的i++x = x + 1。下面是Bob叔叔的一篇文章:FP第1集

因此,这些文章表明,对可变变量的依赖经常会导致副作用,特别是防止我们所谓的"引用透明性",因此,使构建在多线程或更好的多处理器上运行的程序变得更加困难。

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

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

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

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

此外,它会导致"菜鸟"函数式程序员的混淆,因为Scala例如使用了大量的while循环模式,如List .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
  } 

我想Odersky自己说过,他们的目标是API是功能性的,但内部代码是最适合特定实现的。所以你可能不应该在Scala库内部搜索"Scala的好用法"或"FP的好例子"。

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

所以,是的,有一个地方,但练习"生存"尽可能少的可变状态是单线程程序的好做法,因为更少的地方可能出现的错误,更容易测试,更好的可读性。

在一个简单的循环中,没有任何问题——从来没有是并发性的问题,而且您可能可以跟踪变量。嗯,也许吧。

// 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循环中显示的。

相关内容

  • 没有找到相关文章

最新更新