I know the topic has been discussed to death and there are a zillion blogs out there that explain it (and I’ve read half of them). But I’m still unclear on three aspects of self-types that I’d like to discuss.
1
Inheriting from traits breaks encapsulation. The typical example goes like this:
trait S1 { self: S2 =>
def m1 = "m1 using " + m2
// leak not available here
}
trait S2 { self: S3 =>
def m2 = "m2 using " + m3
}
trait S3 {
def m3 = "m3"
def leak = "leak"
}
Indeed, the leak
method would be available in S1
if I were to use inheritance instead (S1 extends S2
and S2 extends S3
). What I don’t understand is why it is not available here. Is there any way for S1
to be used in a context where leak
is not available? Is it hidden on purpose? (“This is not a limitation, it is a feature.”)
2
One argument I’ve seen in favor of self-types is that they make it possible to choose specific module implementations at assembly time. The argument goes like this: If S1 extends S2
, one is committed to this particular S2
at the time S1
is defined. On the other hand, when using S1 { self: S2 =>
, one can pick any refinement of S2
when things are put together.
I don’t understand this argument. It seems to me that I’m free to rely on a specialization S2a
of S2
whether I use self-types or inheritance:
trait S1 { self: S2 =>
def m1 = "m1 using " + m2
}
trait S2 {
def m2 = "m2"
}
trait S2a extends S2 {
override def m2 = "m2a"
}
val s = new S1 with S2a
trait T1 extends T2 {
def m1 = "m1 using " + m2
}
trait T2 {
def m2 = "m2"
}
trait T2a extends T2 {
override def m2 = "m2a"
}
val t = new T1 with T2a
Doesn’t the T
approach let me use an extension T2a
of T2
in the same way the S
approach lets me use S2a
instead of S2
?
3
To switch from inheritance to composition without using self-types, one could pass entire modules as objects to class constructors, something like:
class S1(s2: S2) {
def m1 = "m1 using " + s2.m2
}
class S2(s3: S3) {
def m2 = "m2 using " + s3.m3
}
class S3 {
def m3 = "m3"
}
val s = new S1(new S2(new S3))
The two drawbacks I see are:
- It’s a little harder to deal with cycles in the dependencies.
- There are annoying
s2.
ands3.
indirections everywhere.
Is that it or is there a more fundamental limitation to this approach?
Apologies for the long post, but this looked like the right place to have a good discussion on this.
MC