【问题标题】:Why are the Scala libraries implemented with mutable state?为什么 Scala 库是用可变状态实现的?
【发布时间】:2013-04-22 19:33:51
【问题描述】:

为什么 Scala 标准库中的某些方法以可变状态实现?

例如,find 方法作为scala.Iterator 类的一部分实现为

def find(p: A => Boolean): Option[A] = {
  var res: Option[A] = None
  while (res.isEmpty && hasNext) {
    val e = next()
    if (p(e)) res = Some(e)
  }
  res
}

这可以作为@tailrec'd 方法实现,可能类似于

def findNew(p: A => Boolean): Option[A] = {

  @tailrec
  def findRec(e: A): Option[A] = {
    if (p(e)) Some(e)
    else {
      if (hasNext) findRec(next())
      else None
    }
  }

  if (hasNext) findRec(next())
  else None
}

现在我想一个论点可能是可变状态的使用,而while 循环可能更有效,这在库代码中非常重要,这是可以理解的,但对于@tailrec'd 方法来说真的是这样吗?

【问题讨论】:

  • 您的问题似乎假设如果这些方法是不可变的会更好。是这样吗?毕竟,迭代器本质上是状态引擎。
  • 根据我的(不是广泛的)经验,Scala 和函数式语言通常更喜欢不变性。
  • 嗯,Linq 在表面上是不可变的,但在表面下它使用yield return,它的核心是一个可变状态引擎,就像这个引擎一样。在 Haskell 中,可变性本质上隐藏在 monad 魔法之后。如果你挖掘得足够深,我怀疑你会发现无处不在的可变性。

标签: scala immutability


【解决方案1】:

只要他不共享,拥有可变状态就没有坏处。

在您的示例中,无法从外部访问可变 var,因此该可变变量不可能由于副作用而发生变化。

尽可能地强制执行不变性总是好的,但是当性能很重要时,只要以安全的方式限制可变性就没有什么错。

注意:迭代器是一种没有副作用的数据结构,这可能会导致一些奇怪的行为,但这是另一回事,绝不是以这种方式设计方法的原因。你也会在不可变数据结构中找到类似的方法。

【讨论】:

    【解决方案2】:

    在这种情况下,tailrec 很可能与while 循环具有相同的性能。我想说,在这种情况下,while 循环解决方案更短更简洁。

    但是,无论如何,迭代器都是一个可变的抽象,因此使用尾递归方法来避免该短代码 sn-p 本地的 var 的收益是值得怀疑的。

    【讨论】:

      【解决方案3】:

      Scala 不是为功能纯度而设计的,而是为广泛有用的功能而设计的。其中一部分包括尝试以最有效的方式实现基本库例程(当然不是普遍正确,但通常如此)。

      因此,如果您有两个可能的接口:

      trait Iterator[A] { def next: A }
      trait FunctionalIterator[A] { def next: (A, FunctionalIterator[A]) }
      

      而第二个又笨又慢,选择第一个还是比较明智​​的。

      当一个纯功能的实现对于大部分用例来说都更胜一筹时,您通常会找到纯功能的实现。

      当涉及到简单地使用while 循环与递归时,任何一个都很容易维护,因此这完全取决于编码器的偏好。请注意,findtailrec 的情况下必须标记为final,因此while 保留了更大的灵活性:

      trait Foo {
        def next: Int
        def foo: Int = {
          var a = next
          while (a < 0) a = next
          a
        }
      }
      
      defined trait Foo
      
      
      trait Bar {
        def next: Int
        @tailrec def bar: Int = {
          val a = next
          if (a < 0) bar else a
        }
      }
      
      <console>:10: error: could not optimize @tailrec annotated method bar:
      it is neither private nor final so can be overridden
                   @tailrec def bar: Int = {
                                ^
      

      有一些方法可以解决这个问题(嵌套方法、final、重定向到私有方法等),但它往往会在 while 在语法上更紧凑的地方添加样板。

      【讨论】:

      • 尾递归不会影响灵活性,您可以轻松地拥有一个非最终的 next 方法,该方法调用另一个具有 tailrec 注释 trait Foo { def bar(x: Int) = foo(x); @tailrec final def foo(x: Int): Int = foo(x - 1) } 的私有最终方法
      • @AloisCochard - 好吧,当然,但是你要添加更多样板。 (您可能也希望将其设为私有,这样您的界面就没有两个方法可以做同样的事情。或者您可以将其设为内部函数。)
      • 如果您可以通过这样做来保持灵活性,有时最好稍微冗长一点:-) ...当然在这种特定情况下while更简洁,但总是更喜欢while仅仅因为这个原因而过度尾递归将是一种耻辱。
      • @AloisCochard - 它们的性能相当(while 在某些情况下更容易/更快地进行优化,但tailrec 在某些情况下的热身性能稍好)和对于精通这两种编程风格的人来说,它们几乎同样容易理解。为了简洁/可读性和教条,没有太多可以选择 except 了。鉴于这种选择,我每次都会选择简洁/可读性。 (这并不总是有利于while,但无论它看起来如何,我都会走。)
      • 我完全同意,这也是我倾向于使用的方法。
      猜你喜欢
      • 2016-08-09
      • 2010-10-22
      • 2019-11-23
      • 1970-01-01
      • 1970-01-01
      • 2013-07-19
      • 1970-01-01
      • 1970-01-01
      • 2022-11-30
      相关资源
      最近更新 更多