Search code examples
scalatraitsdeep-copy

How to deep copy classes with traits mixed in


Here's some sample scala code.

abstract class A(val x: Any) {  
    abstract def copy(): A  
}  

class b(i: Int) extends A(i) {  
    override def copy() = new B(x)  
}  

class C(s: String) extends A(s) {  
    override def copy() = new C(x)  
}  

//here's the tricky part  
Trait t1 extends A {  
    var printCount = 0  

    def print = {  
        printCount = printCount + 1  
        println(x)  
    }  

    override def copy = ???  
}  

Trait t2 extends A {  
    var doubleCount = 0  

    def doubleIt = {  
        doubleCount = doubleCount + 1  
        x = x+x  
    }  

    override def copy = ???  
}  

val q1 = new C with T1 with T2  
val q2 = new B with T2 with T1  

OK, as you've likely guessed, here's the question. How can I implement copy methods in T1 and T2, such that weather they are mixed in with B, C, or t2/t1, I get a copy of the whole ball of wax? for example, q2.copy should return a new B with T2 with T1, and q1.copy should return a new C with T1 with T2

Thanks!


Solution

  • Compositionality of object construction

    The basic problem here is, that, in Scala as well as in all other languages I know, object construction doesn't compose. Consider two abstract operations op1 and op2, where op1 makes property p1 true and where op2 makes property p2 true. These operations are composable with respect to a composition operation ○, if op1 ○ op2 makes both p1 and p2 true. (Simplified, properties also need a composition operation, for example conjunction such as and.)

    Let's consider the new operation and the property that new A(): A, that is, an object created by calling new A is of type A. The new operation lacks compositionality, because there is no operation/statement/function f in Scala that allows you to compose new A and new B such that f(new A, new B): A with B. (Simplified, don't think too hard about whether A and B must be classes or traits or interfaces or whatever).

    Composing with super-calls

    Super-calls can often be used to compose operations. Consider the following example:

    abstract class A { def op() {} }
    
    class X extends A {
      var x: Int = 0
      override def op() { x += 1 }
    }
    
    trait T extends A {
      var y: String = "y"
      override def op() { super.op(); y += "y" }
    }
    
    val xt = new X with T
    println(s"${xt.x}, ${xt.y}") // 0, y
    xt.op()
    println(s"${xt.x}, ${xt.y}") // 1, yy
    

    Let X.op's property be "x is increased by one" and let T.op's property be "y's length is increased by one". The composition achieved with the super-call fulfils both properties. Hooooray!

    Your problem

    Let's assume that you are working with a class A which has a field x, a trait T1 which has a field y and another trait T2 which has a field z. What you want is the following:

    val obj: A with T1 with T2
    // update obj's fields
    val objC: A with T1 with T2 = obj.copy()
    assert(obj.x == objC.x && obj.y == objC.y && obj.z == objC.z)
    

    Your problem can be divided into two compositionality-related sub-problems:

    1. Create a new instance of the desired type. This should be achieved by a construct method.

    2. Initialise the newly created object such that all its fields have the same values (for brevity, we'll only work with value-typed fields, not reference-typed ones) as the source object. This should be achieved by a initialise method.

    The second problem can be solved via super-calls, the first cannot. We'll consider the easier problem (the second) first.

    Object initialisation

    Let's assume that the construct method works as desired and yields an object of the right type. Analogous to the composition of the op method in the initial example we could implement initialise such that each class/trait A, T1 and T2 implements initialise(objC) by setting the fields it knows about to the corresponding values from this (individual effects), and by calling super.initialise(objC) in order to compose these individual effects.

    Object creation

    As far as I can see, there is no way to compose object creation. If an instance of A with T1 with T2 is to be created, then the statement new A with T1 with T2 must be executed somewhere. If super-calls could help here, then something like

    val a: A = new A // corresponds to the "deepest" super-call
    val at1: A with T1 = a with new T1
    

    would be necessary.

    Possible solutions

    I implemented a solution (see this gist) based on abstract type members and explicit mixin classes (class AWithT1WithT2 extends A with T1 with T2; val a = new AWithT1WithT2 instead of val a = new A with T1 with T2). It works and it is type-safe, but it is neither particularly nice nor concise. The explicit mixin classes are necessary, because the construct method of new A with T1 with T2 must be able to name the type it creates.

    Other, less type-safe solutions are probably possible, for example, casting via asInstanceOf or reflection. I haven't tried something along those lines, though.

    Scala macros might also be an option, but I haven't used them yet and thus don't know enough about them. A compiler plugin might be another heavy-weight option.