I want to create an enity system with some special properties, based on Scala traits.
The main idea is this: all components are traits that inherit from the common trait:
trait Component
trait ComponentA extends Component
sometimes, in case of a more complex hierarchy and inter-dependant components it can get like this:
trait ComponentN extends ComponentM {
self: ComponentX with ComponentY =>
var a = 1
var b = "hello"
}
and so on. I have come to the conclusion that the data relevant to each component should be contained in itself and not in some storage inside an Entity
or elsewhere because of the speed of the access. As a side note - that is also why everything is mutable, so there is no need in thinking about immutability.
Then Entities
are created, mixing in the traits:
class Entity
class EntityANXY extends ComponentA
with ComponentN
with ComponentX
with ComponentY
Here all is fine, however I do have a special requirement that I do not know how to fulfill with the code. The requirement is this:
Each trait must provide an encoding method(?) that facilitates collection of the trait-related data in a universal form, for example in a form of a JSON or a Map
like Map("a" -> "1", "b" -> "hello")
and a decoding method to translate such a map, if received, back into the trait-related variables. Also: 1) all the encoding and decoding methods of all the mixed-in traits are called in a bunch, in an arbitrary order by Entity
's methods encode
and decode(Map)
and 2) should be made available to be called separately by specifying a trait type, or better, by a string parameter like decode("component-n", Map)
.
It is not possible to use methods with the same name as they will be lost due to shadowing or overriding. I can think of a solution, where all the methods are stored in a Map[String, Map[String, String] => Unit]
for decode and Map[String, () => Map[String, String]]
for encode in every entity. This would work - the by-name as well as a bunch call would certainly be available. However, this will result in storing the same information in every entity which is unacceptable.
It is also possible to store these maps in a companion object so that it is not duplicated anywhere and call the object's encode
and decode
method with an extra parameter denoting a particular instance of the entity.
The requirement may seem strange, but it is necessary because of the required speed and modularity. All of these solutions are clumsy and i think there is a better and idiomatic solution in Scala, or maybe I am missing some important architectural pattern here. So is there any simpler and more idiomatic approach than the one with the companion object?
EDIT: I think that aggregation instead of inheritance could probably resolve these problems but at a cost of not being able to call methods directly on an entity.
UPDATE: Exploring the pretty promising way proposed by Rex Kerr, I have stumbled upon something that hinders. Here is the test case:
trait Component {
def encode: Map[String, String]
def decode(m: Map[String, String])
}
abstract class Entity extends Component // so as to enforce the two methods
trait ComponentA extends Component {
var a = 10
def encode: Map[String, String] = Map("a" -> a.toString)
def decode(m: Map[String, String]) {
println("ComponentA: decode " + m)
m.get("a").collect{case aa => a = aa.toInt}
}
}
trait ComponentB extends ComponentA {
var b = 100
override def encode: Map[String, String] = super.encode + ("b" -> b.toString)
override def decode (m: Map[String, String]) {
println("ComponentB: decoding " + m)
super.decode(m)
m.get("b").foreach{bb => b = bb.toInt}
}
}
trait ComponentC extends Component {
var c = "hey!"
def encode: Map[String, String] = Map("c" -> c)
def decode(m: Map[String, String]) {
println("ComponentC: decode " + m)
m.get("c").collect{case cc => c = cc}
}
}
trait ComponentD extends ComponentB with ComponentC {
var d = 11.6f
override def encode: Map[String, String] = super.encode + ("d" -> d.toString)
override def decode(m: Map[String, String]) {
println("ComponentD: decode " + m)
super.decode(m)
m.get("d").collect{case dd => d = dd.toFloat}
}
}
and finally
class EntityA extends ComponentA with ComponentB with ComponentC with ComponentD
so that
object Main {
def main(args: Array[String]) {
val ea = new EntityA
val map = Map("a" -> "1", "b" -> "3", "c" -> "what?", "d" -> "11.24")
println("BEFORE: " + ea.encode)
ea.decode(map)
println("AFTER: " + ea.encode)
}
}
which gives:
BEFORE: Map(c -> hey!, d -> 11.6)
ComponentD: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24)
ComponentC: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24)
AFTER: Map(c -> what?, d -> 11.24)
The A and B components are not influenced, being cut-off by the inheritance resolution. So this approach is only applicable in certain hierarchy cases. In this case we see that the ComponentD
has shadowed everything else. Any comments are welcomed.
UPDATE 2: I place the comment that answers this problem here, for better reference: "Scala linearizes all the traits. There should be a supertrait of everything which will terminate the chain. In your case, that means that C
and A
should still call super
, and Component
should be the one to terminate the chain with a no-op." – Rex Kerr
Travis had an essentially correct answer; not sure why he deleted it. But, anyway, you can do this without too much grief as long as you're willing to make your encoding method take an extra parameter, and that when you decode you're happy to just set mutable variables, not create a new object. (Complex trait-stacking effectively-at-runtime ranges from difficult to impossible.)
The basic observation is that when you chain traits together, it defines a hierarchy of superclass calls. If each of these calls takes care of the data in that trait, you'd be set, as long as you can find a way to get all that data back. So
trait T {
def encodeMe(s: Seq[String]): Seq[String] = Seq()
def encode = encodeMe(Seq())
}
trait A extends T {
override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "A"
}
trait B extends T {
override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "B"
}
Does it work?
scala> val a = new A with B
a: java.lang.Object with A with B = $anon$1@41a92be6
scala> a.encode
res8: Seq[String] = List(A, B)
scala> val b = new B with A
b: java.lang.Object with B with A = $anon$1@3774acff
scala> b.encode
res9: Seq[String] = List(B, A)
Indeed! Not only does it work, but you get the order for free.
Now we need a way to set variables based on this encoding. Here, we follow the same pattern--we take some input and just go up the super chain with it. If you have very many traits stacked on, you may want to pre-parse text into a map or filter out those parts applicable to the current trait. If not, just pass on everything to super, and then set yourself after it.
trait T {
var t = 0
def decode(m: Map[String,Int]) { m.get("t").foreach{ ti => t = ti } }
}
trait C extends T {
var c = 1
override def decode(m: Map[String,Int]) {
super.decode(m); m.get("c").foreach{ ci => c = ci }
}
}
trait D extends T {
var d = 1
override def decode(m: Map[String,Int]) {
super.decode(m); m.get("d").foreach{ di => d = di }
}
}
And this too works just like one would hope:
scala> val c = new C with D
c: java.lang.Object with C with D = $anon$1@549f9afb
scala> val d = new D with C
d: java.lang.Object with D with C = $anon$1@548ea21d
scala> c.decode(Map("c"->4,"d"->2,"t"->5))
scala> "%d %d %d".format(c.t,c.c,c.d)
res1: String = 5 4 2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With