3

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

它们中的大多数都是以计数器变量递增的不良循环为例。(就像著名的i++or x = x + 1。这里有一篇 Bob 叔叔的文章说明了它:FP Episode 1

因此,这些文章表明,依赖可变变量通常会导致副作用,尤其是会阻止我们所谓的“引用透明”,因此更难构建在多线程或更好的多处理器上运行的程序。

我的问题是:众所周知,i++通常是线程本地变量,因此即使并发处理也不会出现问题。

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

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

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

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

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
  } 
4

2 回答 2

7

我认为 Odersky 本人说过他们的目标是使 API 具有功能性,但内部代码对于特定实现来说是最优化的。因此,您可能不应该在 Scala 库内部搜索“对 Scala 的良好使用”或“FP 的出色示例”。

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

所以,是的,除了练习以尽可能少的可变状态“生存”之外,对于单线程程序来说,这也是很好的做法,因为错误的可能位置更少,更容易测试,可读性更好。

于 2012-12-24T13:16:05.923 回答
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 循环中显示的内容。

于 2012-12-24T13:25:44.867 回答