Preservation of individual traits when mixing them in

I want to create an enity system with some special features based on Scala traits.

The basic idea is this: all components are traits that inherit from a common trait:

trait Component trait ComponentA extends Component 

sometimes, in the case of a more complex hierarchy and interdependent components, it may look something like this:

 trait ComponentN extends ComponentM { self: ComponentX with ComponentY => var a = 1 var b = "hello" } 

etc. I came to the conclusion that the data related to each component should be contained on their own, and not in any storage inside Entity or elsewhere due to the speed of access. As a side note, this is why everything is changeable, so there is no need to think about immutability.

Entities are then created, mixing in features:

 class Entity class EntityANXY extends ComponentA with ComponentN with ComponentX with ComponentY 

Everything is in order here, however, I have a special requirement that I do not know how to execute the code. The requirement is as follows:

Each trait should provide an encoding method (?) That facilitates the collection of data related to traits in a universal form, for example, in the form of JSON or Map , like Map("a" -> "1", "b" -> "hello") , and the decoding method for translating such a mapping, if received, back to the variables associated with it. Also: 1) all encoding and decoding methods of all mixed signs are called in a bunch in an arbitrary manner using the Entity encode and decode(Map) methods and 2) should be available for calling separately specifying the type of attribute or, better, a string parameter, for example decode("component-n", Map) .

It is not possible to use methods with the same name that will be lost due to shadowing or overriding. I can think of a solution where all methods are stored in Map[String, Map[String, String] => Unit] for decoding and Map[String, () => Map[String, String]] for encoding in each object. This will work - a name will probably be available, as well as a call. However, this will lead to the preservation of the same information in each entity, which is unacceptable.

It is also possible to save these cards in a companion object so that it is not duplicated anywhere and decode encode and decode with an additional parameter that indicates a specific instance of the object.

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 Scala has a better and idiomatic solution, or maybe I missed some important architectural pattern here. So, is there a simpler and more idiomatic approach than the one associated with the companion object?

EDIT: I think that aggregation instead of inheritance could probably solve these problems, but at the cost of being unable to invoke methods directly on entities.

UPDATE: While exploring the pretty promising way suggested by Rex Kerr, I came across something that gets in the way. Here is a 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) 

Components A and B are not affected by being clipped by inheritance resolution. Thus, this approach is applicable only in certain cases of the hierarchy. In this case, we see that ComponentD obscures everything else. Any comments are welcome.

UPDATE 2: I am posting a comment that answers this issue here for a better reference: "Scala linearizes all the traits. There must be a superwrite of everything that ends the chain. Your case, this means that C and A should still call super , and Component is the one that ends the chain with no-op. " - Rex Kerr

+7
source share
1 answer

Travis had essentially the correct answer; I don’t know why he deleted it. But, in any case, you can do this without much grief, as long as you are ready to make your encoding method an additional parameter, and that when decoding, you can simply set mutable variables and not create a new object. (A sophisticated set of stacking labels effectively in standby mode ranges from difficult to impossible.)

The main observation is that when combining features together, it defines the hierarchy of calls to the superclass. If each of these calls takes care of the data in this attribute, you should be set until you can find a way to return all this data. 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 he 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) 

Really! This not only works, but also receives an order for free.

Now we need a way to set variables based on this encoding. Here we follow the same scheme - we take some contribution and just go to the super chain with it. If you have too many features, you can pre-analyze the text on the map or filter out those parts that are applicable to the current feature. If not, just pass everything on to super, and then set yourself up 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 also works as we would like:

 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(ct,cc,cd) res1: String = 5 4 2 
+5
source

All Articles