So this is a fairly direct port of this Java question to scala
We have a bunch of traits that take generic parameters as follows:
trait Ident { }
trait Container[I <: Ident] {
def foo(id: I): String
}
trait Entity[C <: Container[I], I <: Ident] {
def container: C
def foo(id: I) = container.foo(id)
}
This works but it's a little clumbsy, since we have to provide the type of the Ident and the type of the Container when defining a sub-class of Entity. When in fact just the type of the Container would be enough type information by itself:
class MyIdent extends Ident { }
class MyContainer extends Container[MyIdent] { }
class MyEntity extends Entity[MyContainer,MyIdent] { }
// ^^^^^^^ shouldn't really be necessary
Using an existential type avoids the need for Entity to take two parameters ... but of course you can't refer to it later on.
trait Entity[C <: Container[I] forSome { type I <: Ident }] {
def container: C
def foo(id: I) = container.foo(id)
// ^^^ complains it has no idea what 'I' is here
}
Similarly converting the thing to use member types also doesn't work ...
trait Ident { }
trait Container {
type I <: Ident
def foo(id: I): String
}
trait Entity {
type C <: Container
def container: C
def foo(id: C#I) = container.foo(id)
// ^^ type mismatch
}
So does anyone know if there's an elegant solution to this problem in Scala?
Update given this answer I'm not sure whether this should be considered a bug or not
You've hit SI-4377; if you provide explicit type ascriptions you'll get an error which I'm guessing just exposes that type projections are implemented using existentials:
trait Ident { }
trait Container {
type I <: Ident
def foo(id: I): String
}
trait Entity {
type C <: Container
def container: C
def foo(id: C#I): String = (container: C).foo(id: C#I)
// you will get something like: type mismatch;
// [error] found : Entity.this.C#I
// [error] required: _3.I where val _3: Entity.this.C
// as I said above, see https://issues.scala-lang.org/browse/SI-4377
}
It is not an understatement to say that this (bug?) makes generic programming with type members a nightmare.
There is a hack though, which consists in casting values to a hand-crafted self-referential type alias:
case object Container {
type is[C <: Container] = C with Container {
type I = C#I
// same for all other type members, if any
}
def is[C <: Container](c: C): is[C] = c.asInstanceOf[is[C]]
}
Now use it and Entity
compiles:
trait Entity {
type C <: Container
def container: C
def foo(id: C#I): String = Container.is(container).foo(id)
// compiles!
}
This is of course dangerous, and as a rule of thumb it is safe only if C
and all its type members are bound to a non-abstract type at the point it will be used; do note that this will not always be the case, as Scala lets you leave "undefined" type members:
case object funnyContainer extends Container {
// I'm forced to implement `foo`, but *not* the `C` type member
def foo(id: I): String = "hi scalac!"
}
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