Avoiding redundant generic parameters in Scala
Asked Answered
O

1

9

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?

Omdurman answered 9/11, 2016 at 15:59 Comment(4)
I don't think the answer will be much different than the Java version. There's not really a way to omit the type parameter without losing the type information that would come with it.Encode
Is it an option to make container a val?Cornet
@MichaelZajac So in the definition of "MyEntity" the second parameter provided to Entity is redundant: there is simply no other possible type you could use there other than "MyIdent", quite literally anything else will give compilation errors. Of course, whether you can avoid that redundancy in Scala is another question :-)Omdurman
Check this answer for how something like what I proposed in my answer can lead to unsoundnessBeaird
B
4

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!"
}
Beaird answered 9/11, 2016 at 18:2 Comment(1)
Thank you, this works. Glad to know I had the right idea but just hit a bug :-)Omdurman

© 2022 - 2024 — McMap. All rights reserved.