I'm having a problem modelling our domain with immutable objects.
Mutable design
One base trait WObject (world object) and loads of traits for implementing specific operations like OwnedObj (hp/owner/takeDamage), Movable (movementLeft/moveTo), Fighter (attacked/attack).
At the end of the hierarchy you have a mutable class which mixes in the appropriate traits:
class Corvette(var position: Vect2) extends WObject with OwnedObj with Movable with Fighter
If a user wants to do an operation (lets say move a ship), you do:
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
}
Immutable design
If moveTo has to return a new object, what type does it return?
I've tried using trait Movable[Self <: Movable[Self]]
approach, but this requires to take Movable[_] everywhere and those existential types get out of hand quickly. What if I want Movable[_] with Fighter[_]
? Are _ the same type?
I've also tried abstract type Self inside the traits with type bounds approach, but this starts to get hairy in following scenarios:
def takeDamage(obj: OwnedObj): obj.Self = if (Random.nextDouble()) obj.takeDamage else obj.self
Nest this a bit and you get types like
def attackReachable(
data: WObject.WorldObjUpdate[Self]
): WObject.WorldObjUpdate[data.value._2.Self]
Which are just horrible.
I was thinking about ditching inheritance and using composition + typeclasses, but I'm not quite sure how to do it.
For example:
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
}
And then define operations in typeclasses, which would be implemented in Corvette companion object.
But I am not sure how to specify the restrains.
More over how do you implement the move operation from client side?
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)
}
Help appreciated :)
Related: Polymorphic updates in an immutable class hierarchy