Avoiding redundant generic parameters in Scala

2019-04-24 09:22发布

问题:

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?

回答1:

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