Tom Shackell Tom Shackell - 28 days ago 19
Scala Question

Avoiding redundant generic parameters in Scala

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?

Answer

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!"
}