If I have the following Scala type hierarchy:
// Base traits
trait TA[X <: TA[X,Y], Y <: TB[X,Y]]
trait TB[X <: TA[X,Y], Y <: TB[X,Y]]
trait TC[X <: TA[X,_]]
// More specific traits
trait TI[X <: TI[X,Y], Y <: TJ[X,Y]] extends TA[X,Y]
trait TJ[X <: TI[X,Y], Y <: TJ[X,Y]] extends TB[X,Y]
trait TK[X <: TI[X,_]] extends TC[X]
// Concrete class that should implement TK, but does not compile
class Z extends TK[TI[_,_]]
// What is needed to be able to get class Z to compile
// The reference of X to itself has been removed.
trait TC2[X <: TA[_,_]]
trait TK2[X <: TI[_,_]] extends TC2[X]
class Z2 extends TK2[TI[_,_]]
TC would be a generic manager of some sort of TA.
TK would be a more specific manager of a more specific TA (TI).
Z would be the concrete implementation that manages any object implementing TI.
Z is not legal, but Z2 is. Unfortunately, TC and TK are more specific than TC2 and TK2. So is there a way of declaring Z using TC and TK, instead of TC2 and TK2?
[EDIT] I failed to say in my original question, that I kind of understand why Z is unsound. What I really want to know is, if there is a way of saying something like:
class Z3 extends TK[TI[TI,_]]
Language. Methods in Scala can be parameterized by type as well as by value. The syntax is similar to that of generic classes. Type parameters are enclosed in square brackets, while value parameters are enclosed in parentheses.
A class's static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed.
For example, a type constructor does not directly specify a type of values. However, when a type constructor is applied to the correct type arguments, it yields a first-order type, which may be a value type. Non-value types are expressed indirectly in Scala.
It means an abstract type member is defined (inside some context, e.g. a trait or class), so that concrete implementations of that context must define that type.
When you have complex mutually recursive type bounds it can often be helpful see if you can translate your problem into an equivalent one using abstract type members instead. If we do that mechanically, your base and more specific traits end up looking like,
// Base traits
trait TA {
type X <: TA
type Y <: TB
}
trait TB {
type X <: TA
type Y <: TB
}
trait TC {
self =>
type X <: TA { type X <: self.X }
}
// More specific traits
trait TI extends TA {
type X <: TI
type Y <: TJ
}
trait TJ extends TB {
type X <: TI
type Y <: TJ
}
trait TK {
self =>
type X <: TI { type X <: self.X }
}
and now we have a straightforward definition of Z as,
class Z extends TK {
type X = TI
}
Note that the definitions of the members of TA, TB and TI, TJ are essentially the same. Because these types are now type members we can factor them out into common base types like so,
// Base traits
trait T0 {
type X <: TA
type Y <: TB
}
trait TA extends T0
trait TB extends T0
trait TC {
self =>
type X <: TA { type X <: self.X }
}
// More specific traits
trait T1 extends T0 {
type X <: TI
type Y <: TJ
}
trait TI extends TA with T1
trait TJ extends TB with T1
trait TK extends TC {
self =>
type X <: TI { type X <: self.X }
}
class Z extends TK {
type X = TI
}
It would be unsound. Here is why, with a simplified example. We do not need two generic parameters, we do not need the subtypes TI,TJ and TK either
trait TA[X <: TA[X]]
trait TC[X <: TA[X]]
class Z extends TC[TA[_]]
type arguments [TA[_]] do not conform to trait TC's type parameter bounds [X <: TA[X]]
Let's see why this declaration is unsound, and it is proper that it fails.
Let's add some code. I change TC
to a class
, so that the code can be translated to java. A trait
would do as well in scala.
trait TA[X <: TA[X]] {def f(x: X) }
class TC[X <: TA[X]] {def g(x: X) = x.f(x)}
It works fine, x: X
is also TA[X]
, so there it has a routine f
, which will accept an X
.
If we try instead
class TC2[X <: TA[_]] {def g(x: X) = x.f(x)}
then it fails. We know there is an f
method on x
, but we do not know what type is needed as an argument, we cannot know that x
will be ok. Indeed, suppose we define
class T1 extends TA[T1]] {def f(t1: T1) = {}; def t1Only = println("only in T1")}
class T2 extends TA[T1]] {def f(t1: T1) = t1.t1Only }
Now, if TC2
was allowed, I could create a TC2[T2]
, call g
with a T2
, which would call f
in T2
with a T2
. This is not allowed, and rightly so as T2
has no method t1Only
.
This shows why TC
cannot be accepts TA[_]]
as its parameter, as that would allow T2
, which is not compatible with method g
. And so why we cannot define Z
with parameter TA[_]
. It would be the exact same in java, and with your original code as well.
Edit: I feel a little guilty about my answer. With the reason I gave why it should not be allowed, I thought there would be a simple workaround. It failed, I had no time to investigate further, and posted without mentioning it. The workaround was a self type. If we do
trait TA[X <: TA[X]] {self: X => }
then we cannot define T2 extends TA[T1]
. So it is more limited that the original code. But we have to accept limitations to the original code anyway, because it was unsound. So it cannot be just a syntax trick, it has to make things impossible that were not. I thought T2 extends TA[T1]
was likely not something that was intended, and that it was the one thing to prevent.
Apparently, it was not, same error. Now I do not have an example why it should not work. Which of course does not means there is none.
I then had a look at Miles' solution, wondering why the possibility of T2 extends TA[T1]
does not hurt it. So again, discarding TB
, Y
, TI
, TJ
, and TK
:
trait TA{type X}
trait TC{self => type X <: TA{type X <: self.X}
class Z extends TC{type X = TA}
This compiles. And we can do
trait T1 extends TA{type X = T1}
trait T2 extends TA{type X = T1}
But there is one thing we cannot do:
trait TA {type X <: TA; def f(x: X)}
trait TC {self =>
type X <: TA{type X <: self.X}
def g(x: X) = x.f(x)
}
We get the following error on g
, for the x
argument to f
type mismatch; found : x.type (with underlying type TC.this.X) required: x.X
TC
is not exactly the original one (as originally, g
was allowed). It is so because there is a type X <: self.X
on TA
while TC[X <: TA[X]]
was the stronger type X = self.X
. If we write that instead, we are back to the original error, Z
does not compile. So this TC
is somewhat between the original TC
(type X = self.X
) and TC2
(no knowledge of the X of TA). Again, a limitation on the original code, we cannot define g
.
If the limitation is acceptable, you're ok. I do not know how to write it as a generic (nor how to write the self type {self : X =>
with an abstract type member) . Miles is definitely the expert, I'm sure he would be able to tell either how it is done or that it is not possible.
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