Scala:收集不可变状态的更新/更改

blu*_*e10 19 state design-patterns functional-programming scala immutability

我目前正在尝试将更多功能的编程风格应用于涉及低级(基于LWJGL)的GUI开发的项目.显然,在这种情况下,需要携带很多状态,这在当前版本中是可变的.我的目标是最终拥有一个完全不可变的状态,以避免状态变化作为副作用.我研究了scalaz的镜头和状态monad一段时间,但我主要担心的是:所有这些技术都依赖于写时复制.由于我的州有大量的田地和一些相当大的田地,我担心表现.

据我所知,修改不可变对象的最常用方法是使用生成的copy方法case class(这也是镜头所做的内容).我的第一个问题是,这种copy方法是如何实际实现的?我用以下类进行了一些实验:

case class State(
  innocentField: Int, 
  largeMap: Map[Int, Int], 
  largeArray: Array[Int]
)
Run Code Online (Sandbox Code Playgroud)

标杆管理,并通过查看输出-Xprof,它看起来像更新someState.copy(innocentField = 42)实际进行深拷贝和我观察显著性能下降,当我增加的大小largeMaplargeArray.我某种程度上期望新构造的实例共享原始状态的对象引用,因为内部引用应该只传递给构造函数.我可以以某种方式强制或禁用默认的这种深度复制行为copy吗?

在思考写时复制问题时,我想知道FP中是否存在更多通用的解决方案,它以一种增量方式存储不可变数据的变化(在"收集更新"或"收集"的意义上)变化").令我惊讶的是我找不到任何东西,所以我尝试了以下方法:

// example state with just two fields
trait State {
  def getName: String
  def getX: Int

  def setName(updated: String): State = new CachedState(this) {
    override def getName: String = updated
  }
  def setX(updated: Int): State = new CachedState(this) {
    override def getX: Int = updated
  }

  // convenient modifiers
  def modName(f: String => String) = setName(f(getName))
  def modX(f: Int => Int) = setX(f(getX))

  def build(): State = new BasicState(getName, getX)
}

// actual (full) implementation of State
class BasicState(
  val getName: String, 
  val getX: Int
) extends State


// CachedState delegates all getters to another state
class CachedState(oldState: State) extends State {
  def getName = oldState.getName
  def getX    = oldState.getX
}
Run Code Online (Sandbox Code Playgroud)

现在这允许做这样的事情:

var s: State = new BasicState("hello", 42)

// updating single fields does not copy
s = s.setName("world")
s = s.setX(0)

// after a certain number of "wrappings"
// we can extract (i.e. copy) a normal instance
val ns = s.setName("ok").setX(40).modX(_ + 2).build()
Run Code Online (Sandbox Code Playgroud)

我现在的问题是:你怎么看待这个设计?这是我不知道的某种FP设计模式(除了与Builder模式的相似性)?由于我没有找到类似的东西,我想知道这种方法是否存在一些重大问题?或者是否有更多标准方法可以解决写时复制瓶颈而不放弃不变性?

是否有可能以某种方式统一get/set/mod函数?

编辑:

copy执行深层复制的假设确实是错误的.

Rex*_*err 12

这与视图基本相同,是一种惰性评估; 这种类型的策略或多或少是Haskell中的默认策略,并且在Scala中使用了相当一部分(参见例如地图上的mapValues,分组集合,几乎任何关于Iterator或Stream的返回另一个Iterator或Stream的内容等).在正确的背景下避免额外工作是一种行之有效的策略.

但我认为你的前提有点错误.

case class Foo(bar: Int, baz: Map[String,Boolean]) {}
Foo(1,Map("fish"->true)).copy(bar = 2)
Run Code Online (Sandbox Code Playgroud)

实际上并不会导致地图被深深复制.它只是设置引用.字节码证明:

62: astore_1
63: iconst_2   // This is bar = 2
64: istore_2
65: aload_1
66: invokevirtual   #72; //Method Foo.copy$default$2:()Lscala/collection/immutable/Map;
69: astore_3   // That was baz
70: aload_1
71: iload_2
72: aload_3
73: invokevirtual   #76; //Method Foo.copy:(ILscala/collection/immutable/Map;)LFoo;
Run Code Online (Sandbox Code Playgroud)

让我们看看那copy$default$2件事是做什么的:

0:  aload_0
1:  invokevirtual   #50; //Method baz:()Lscala/collection/immutable/Map;
4:  areturn
Run Code Online (Sandbox Code Playgroud)

只需返回地图.

而且copy呢?

0:  new #2; //class Foo
3:  dup
4:  iload_1
5:  aload_2
6:  invokespecial   #44; //Method "<init>":(ILscala/collection/immutable/Map;)V
9:  areturn
Run Code Online (Sandbox Code Playgroud)

只需调用常规构造函数.没有克隆地图.

因此,当您复制时,您只需创建一个对象 - 您正在复制的内容的新副本,并填写字段.如果您有大量字段,您的视图将更快(因为您必须创建一个新对象) (如果您使用函数应用程序版本,则两个,因为您还需要创建函数对象)但它只有一个字段).否则它应该是一样的.

所以,是的,好主意可能,但仔细测试以确保它在你的情况下是值得的 - 你必须手工编写一些代码,而不是让案例类为你做所有.