纯度,参考透明度和国家Monad

时间:2015-06-01 23:55:53

标签: scala functional-programming

我目前正在设计一种数值算法,作为其操作的一部分,需要多次更新doubles的向量。由于算法必须尽可能节省空间和时间,因此我不想对传统类型的FP代码进行编码,这些FP代码在每次操作后都会在引擎盖下创建许多版本的数据结构。我也不想创建可变数据结构并使它们全局可用。因此,我决定使用可变数据结构,但随后选择在State monad中执行可变操作。由于这是我第一次使用State monad,我想确认我是否有

  1. 保留参考透明度
  2. 保持功能纯度
  3. update函数转换数据结构状态。由于破坏性更新已本地化在此函数中,并且无法从外部获取数据结构的句柄,因此我认为此函数是纯粹的且引用透明。

    def update(i : Int,d : Double) = State[ArraySeq[Double], Unit]{
      case xs: ArraySeq[Double] => {xs(i) = d; (xs, ())}
    }
    

    app函数是一个玩具函数,它会消耗double个序列并修改它的状态:

    def app : State[ArraySeq[Double], Unit] = for{
        _ <- update(0, 3.142)
      // do a heap of stuff on ArraySeq
    }yield()
    

    呼叫:

    app(Vector(0.0, 1.0, 2.0, 3.0, 4.0).to[ArraySeq])._1.to[Vector]
    

    结果:

    res0: Vector[Double] = Vector(3.142, 1.0, 2.0, 3.0, 4.0)
    

1 个答案:

答案 0 :(得分:11)

我想你可以说你的update本身是纯粹的,因为只有代表一些变异,但是一旦你运行它所有的赌注都会关闭:

scala> val xs = List(1.0, 2.0, 3.0).to[ArraySeq]
xs: scala.collection.mutable.ArraySeq[Double] = ArraySeq(1.0, 2.0, 3.0)

scala> update(0, 10).eval(xs)
res0: scalaz.Id.Id[Unit] = ()

scala> xs
res1: scala.collection.mutable.ArraySeq[Double] = ArraySeq(10.0, 2.0, 3.0)

这是一个糟糕的场景,它与纯粹或引用透明相反。

State并不是真的在你的例子中为你买了任何东西 - 事实上你以app的方式调用ArraySeq而其他任何人都无法改变的是。您也可以咬住子弹并在您控制的范围内以通常的方式使用可变数据结构 - 即,像这样写app

def app(xs: Vector[Double]): Vector[Double] = {
  val arr = xs.to[ArraySeq]
  // Perform all your updates in the usual way
  arr.toVector
}

这实际上 纯粹且引用透明,但它也比State版本更诚实。如果我看到State[Foo, Unit]类型的值,我的假设是该值表示某种操作会将Foo更改为新的Foo而不会改变原始Foo。这是状态monad的所有状态 - 它提供了一种很好的方法,可以对不可变数据结构进行建模操作,并以类似于变异的方式组合它们。如果你将它与实际的突变混合在一起,你很可能会混淆任何使用你代码的人。

如果你真的想要同时真正的变异和纯度,你可以看看Scalaz的STArray。这是一个非常聪明的解决方案,在像Haskell这样的语言中,这是一种很有意义的方法。我自己的感觉是,它在Scala中几乎总是错误的解决方案。如果你真的需要一个可变数组的性能,只需使用一个本地可变数组,并确保你不泄漏到外部世界。如果您不需要这种性能(大多数时候不需要),请使用State之类的内容。