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!
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).
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!
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:
Create a new instance of the desired type. This should be achieved by a construct
method.
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.
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.
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.
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.