gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 12:55:18 27 4
gpt4 key购买 nike

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

据我所知,修改不可变对象(immutable对象)的最常用方法是使用 copy 生成的 case class 方法(这也是镜头在引擎盖下所做的)。我的第一个问题是,这个 copy 方法实际上是如何实现的?我对一个类进行了一些实验,例如:

case class State(
innocentField: Int,
largeMap: Map[Int, Int],
largeArray: Array[Int]
)

通过基准测试以及查看 -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
}

现在这允许做这样的事情:
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()

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

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

编辑:

我认为 copy 执行深拷贝的假设确实是错误的。

最佳答案

这与 View 基本相同,是一种惰性求值;这种类型的策略或多或少是 Haskell 中的默认策略,并且在 Scala 中使用得相当多(参见例如 mapValues on maps、grouped on collections、Iterator 或 Stream 上几乎所有返回另一个 Iterator 或 Stream 的东西等)。这是一种行之有效的策略,可以避免在正确的环境中进行额外的工作。

但我认为你的前提有些错误。

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

实际上不会导致 map 被深度复制。它只是设置引用。字节码证明:
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;

让我们看看 copy$default$2事情做:
0:  aload_0
1: invokevirtual #50; //Method baz:()Lscala/collection/immutable/Map;
4: areturn

只返回 map 。

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

只需调用常规构造函数。没有克隆 map 。

因此,当您复制时,您只创建了一个对象——您正在复制的内容的新副本,其中填写了字段。如果您有大量字段,您的 View 会更快(因为您必须创建一个新对象(如果您使用功能应用程序版本,则为两个,因为您还需要创建功能对象)但它只有一个字段)。否则应该差不多。

所以,是的,可能是个好主意,但要仔细进行基准测试,以确保它在你的案例中是值得的——你必须手工编写相当多的代码,而不是让案例类为你做这一切。

关于Scala:收集不可变状态的更新/更改,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13260162/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com