关于scala:使用不可变对象模拟继承+可变状态

Simulating inheritance+mutable state with immutable objects

我在用不变的对象建模我们的领域时遇到了一个问题。

可变设计

一个基本特征wObject(World Object)和用于执行特定操作的大量特征,如ownedobj(HP/Owner/TakeDamage)、moved(movementLeft/moveto)、fighter(attacked/attack)。

在层次结构的末尾,有一个可变类,它混合了适当的特性:

1
class Corvette(var position: Vect2) extends WObject with OwnedObj with Movable with Fighter

如果用户想要执行操作(比如移动船舶),则需要执行以下操作:

1
2
3
4
val opt = objects.collectFirst { case obj: Movable if obj.position == position => obj }
opt.fold(Log.error(s"Movable at $position not found!")) { obj =>
  obj.moveTo(position) // return type is Unit
}

不变的设计

如果moveto必须返回一个新对象,它返回什么类型?

我曾经尝试过使用trait Movable[Self <: Movable[Self]]方法,但这需要把可移动的(uuux)放在任何地方,而那些存在的类型很快就会失控。如果我想要Movable[_] with Fighter[_]怎么办?是同一种类型吗?

我还尝试了在traits中使用类型边界方法抽象类型self,但是在以下场景中,这开始变得复杂起来:

1
def takeDamage(obj: OwnedObj): obj.Self = if (Random.nextDouble()) obj.takeDamage else obj.self

把这个嵌套一点,你就会得到

1
2
3
  def attackReachable(
    data: WObject.WorldObjUpdate[Self]
  ): WObject.WorldObjUpdate[data.value._2.Self]

太可怕了。

我在考虑放弃继承并使用composition+typeclasss,但我不太确定该怎么做。

例如:

1
2
3
4
5
6
7
8
9
10
case class WObject(position: Vect2, id: UUID=UUID.randomUUID())
case class OwnedObj(owner: Owner)
case class Movable(movementLeft: Int)
case class Fighter(attacked: Boolean)
case class Corvette(obj: WObject, owned: OwnedObj, movable: Movable, fighter: Fighter)

// Something that has both WObject and Movable
trait MovableOps[A <: ???] {
  def moveTo(obj: A, target: Vect2): A
}

然后在typeclasses中定义操作,这些操作将在CorvetteCompanion对象中实现。

但是我不知道如何指定限制条件。

关于如何从客户端实现移动操作的更多信息?

1
2
3
4
val opt = objects.collectFirst { case obj: ??? if obj.position == position => obj }
opt.fold(Log.error(s"Movable at $position not found!")) { obj =>
  objects = objects - obj + obj.moveTo(position)
}

感谢您的帮助:)

相关:不可变类层次结构中的多态更新


你可以用一个存在主义的例子来写你的"同一个"案例:(Movable[T] with Fighter[T] forSome {type T})

如果我正确理解了您的attackReachable示例,我不会太担心依赖路径的类型。通常可以允许推断它们,具体调用将具有"实际"类型。策略性地使用隐含的=:=Leibniz参数,如果您知道类型实际上是相同的,可以阻止事情失控。或者更简单地说,您可以只要求类型相同:

1
2
def doSomething[T <: Moveable { type Self = T }](t: T): T =
  t.somethingThatReturnsTDotSelf()

如果你想走合成路线,我能想到的最好的方法是使用无形状镜片(我不能与单镜头相比,因为我没有使用过它们):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
trait Move[A] {
  val lens: Lens[A, (WObject, Movable)]
}
/** This could be implicitly derived with Generic if you really want to -
or you could use Records. */

implicit def moveCorvette = new Move[Corvette] {
  val lens = lens[Corvette].obj ~ lens[Corvette].movable
}

def moveTo[A: Move](obj: A, target: Vect2) = {
  val l = Lens[A, (Wobject, Movable)]
  val remainingMoves = l.get(obj)._2.movementLeft - 1
  l.set(obj)((target, remainingMoves))
}

要将此应用于列表,您可以将列表保留为HLIST,以便了解所有元素的类型(例如,您的列表是Fighter :: Corvette :: HNil类型),或者将证据包含在具有存在性(例如trait ObjAndMove {type T; val obj: T; val evidence: Move[T]}类型)的列表条目中,然后使用List[ObjAndMove]