19

I'd like to be able to assemble domain objects from traits, according to various properties that the concrete classes might have. When my objects are mutable, this is pretty straightforward. For example:

trait HasHitPoints { var hitPoints: Int = 100 }
trait HasBearing { var bearing: Double = 0 }

class Ship extends HasHitPoints with HasBearing
class Base extends HasHitPoints

val entities = new Ship :: new Base :: Nil
entities.collect { case h: HasHitPoints => h.hitPoints += 10 }

In particular, I can polymorphically read or update any HasHitPoints instance without knowing the concrete type.

What is the best way to implement this with immutable objects? If I'm happy to just read the properties, then I could do something like:

trait HasHitPoints { val hitPoints: Int }
trait HasBearing { val bearing: Double }

case class Ship(hitPoints: Int, bearing: Double) extends HasHitPoints with HasBearing
case class Base(hitPoints: Int) extends HasHitPoints

val things = Ship(50, 0) :: Base(100) :: Nil

val totalHitPoints = things.collect { case h: HasHitPoints => h.hitPoints }.sum

Also, I can easily modify the concrete classes using copy if I know the precise type. The hard part is updating an arbitrary HasHitPoints, for example. If I have lots of concrete classes, and lots of different properties I might like to mix-in, what's the best scheme to avoid an explosion of boilerplate code?

Matt R
  • 9,892
  • 10
  • 50
  • 83

3 Answers3

1

You may have some luck with adding an e.g. abstract def withHitPoints(points: Int) method to your traits, which returns a copy of the container object with a different property value. This reduces the usage to something like:

val damagedActors = actors map { actor => actor.withHitPoints( actor.hitPoints - 10 ) }

But will otherwise require an extra method per property per concrete class, so I'm not sure it really solves your problem. This does not feel right for a static language like Scala (nor would I likely bother with immutability for this particular use-case); an immutable solution here may be a better candidate for a dynamic language.

Tomer Gabel
  • 4,104
  • 1
  • 33
  • 37
  • Right, although the clutter of N update methods in M concrete classes was what I was hoping to avoid. And as you point out, it'd be pretty straightforward in a dynamic language. This use case is obviously very simplified, but I think the issue often crops up when you mix updates with an immutable inheritance hierarchy. – Matt R Jan 10 '12 at 13:17
  • I won't argue that it's something Scala doesn't handle well, but in my experience it's a scenario that doesn't crop up often - unless of course you're dead-set on this time of modeling. – Tomer Gabel Jan 11 '12 at 13:32
  • 2
    Possibly, although I wonder to what degree that's the Sapir-Whorf thing: the scenarios a language can't handle well become the scenarios that don't crop up often... – Matt R Jan 11 '12 at 14:28
  • @TomerGabel Strange because I immediately encountered this scenario in the first larger-scale Scala program I worked on. Polymorphism based on inheritance hierarchies is one of the most important concepts in building complex systems and this limitation just cuts one of its legs off, because you can only use it on query methods that do not do any data updates. You end up either settling for plain old mutable data types, or fiddling with type members, generics and companion objects to try to achieve the same thing. – jbx Jan 06 '15 at 14:36
1

You are hoping to avoid the N update methods in M concrete classes. However painful I think this is just not possible. You will need access to the copy method or at least the constructor of every concrete class. Neither of them can be abstracted as is also discussed in: Case class copy() method abstraction So in the end you will always end up with the N x M 'boilerplate' code.

Community
  • 1
  • 1
0

Sorry for the necromancy, but it is worth pointing that this is doable using F-bounded polymorphism:

  trait HasHitPoints[Self <: HasHitPoints[Self]] { 
    val hitPoints: Int 
    def updateHitpoints(f: Self => Int): Self
  }
  trait HasBearing { val bearing: Double }

  case class Ship(hitPoints: Int, bearing: Double)
      extends HasHitPoints[Ship]
      with HasBearing {
    override def updateHitpoints(f: Ship => Int): Ship = copy(hitPoints = f(this))
  }
  case class Base(hitPoints: Int) extends HasHitPoints[Base] {
    override def updateHitpoints(f: Base => Int): Base = copy(hitPoints = f(this))
  }

  val things = Ship(50, 0) :: Base(100) :: Nil

  val heal = things.map(_.updateHitpoints(_.hitPoints + 10))

  val totalHitPoints = heal.map(_.hitPoints).sum
V-Lamp
  • 1,630
  • 10
  • 18