【问题标题】:Avoid reassignment in functional programming - Bad example?避免在函数式编程中重新分配 - 不好的例子?
【发布时间】:2012-12-24 12:55:31
【问题描述】:

在许多网络文章中,函数式编程被描述为避免所有类型的变量重新分配,因此至少只推广“最终”变量,以便更好地阅读。

它们中的大多数都取自一个计数器变量递增的不良循环的样本。 (如著名的i++x = x + 1。 这是 Bob 叔叔的一篇文章说明它:FP Episode 1

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

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

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

为什么不呢,为了更清楚,选择一个全局变量(或字段对象)的重新分配,这显然是并发编程的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
  } 

【问题讨论】:

  • 这就是为什么 scala 是一种混合语言 - 它使用最适合任务的任何东西,偏向于函数式风格。可变计数器的例子就像你说的 - 一个糟糕的例子。没有什么会使可变状态在任何情况下都变得不好(除非你是一个纯粹主义者,而且看起来他们毕竟是对的),只是很难划清区分“好的可变状态”和“坏的可变状态”的界限因此最好完全避免它。您制造的例外越少,就越容易理解您想要实现的目标。
  • 这也可能不是关于 SO 的最佳问题 :)
  • 我认为 Odersky 本人说过,他们的目标是使 API 具有功能性,但内部代码对于特定实现来说是最优化的。
  • @soulcheck,它并没有真正征求意见。我可能忽略的是多核编程的特殊情况。我目前正在搜索是否在任何特定情况下,两个处理器都可以共享局部变量......因此解释了循环样本及其可能涉及的风险。顺便说一句,我同意你的第一条评论:)
  • 在循环中使用可变计数器会使循环并行化变得更加困难。如果您使用了mapfold,我们可以将其替换为平行地图或折叠。

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


【解决方案1】:

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

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

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

p>

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-09-26
      • 2021-01-11
      • 2013-10-21
      • 1970-01-01
      • 1970-01-01
      • 2010-09-19
      • 1970-01-01
      相关资源
      最近更新 更多