纯度、引用透明度和状态 Monad

Purity, Referential Transparency and State Monad

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

  1. 保留引用透明度
  2. 保持功能纯度

update函数转换数据结构状态。由于破坏性更新局限于此函数内,并且无法从外部获取数据结构的句柄,因此我认为此函数是纯粹的且引用透明的。

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

app 函数是一个玩具函数,它将使用一系列 doubles 并修改它的状态:

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)

我想你可以说你的 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 的全部——它提供了一种很好的方法来对不可变数据结构上的操作进行建模,并以一种看起来有点像突变的方式组合它们。如果您将它与 actual 突变混合使用,您可能会将使用您的代码的任何人搞混。

如果你真的想要同时真正的突变和纯度,你可以看看Scalaz的STArray。对于这个问题,这是一个非常聪明的解决方案,在像 Haskell 这样的语言中,这是一种很有意义的方法。不过,我个人的感觉是,在 Scala 中它几乎总是错误的解决方案。如果您确实需要可变数组的性能,只需使用本地可变数组并确保不会将其泄露给外界。如果您不需要那种性能(大多数时候不需要),请使用 State.

之类的东西