31

I'm trying to use a covariant type parameter inside a trait to construct a case-class like so:

trait MyTrait[+T] {
  private case class MyClass(c: T)
}

compiler says:

error: covariant type T occurs in contravariant position in type T of value c

I then tried the following but it also didn't work:

trait MyTrait[+T] {
  private case class MyClass[U <: T](c: U)
}

the error this time is:

error: covariant type T occurs in contravariant position in type >: Nothing <: T of type U

Could somebody explain why the T is in a covariant position here and suggest a solution for this problem? Thx!

lapislazuli
  • 506
  • 4
  • 6

2 Answers2

61

This is a fundamental feature of object-oriented programming that doesn't get as much attention as it deserves.

Suppose you have a collection C[+T]. What the +T means is that if U <: T, then C[U] <: C[T]. Fair enough. But what does it mean to be a subclass? It means that every method should work that worked on the original class. So, suppose you have a method m(t: T). This says you can take any t and do something with it. But C[U] can only do things with U, which might not be all of T! So you have immediately contradicted your claim that C[U] is a subclass of C[T]. It's not. There are things you can do with a C[T] that you can't do with a C[U].

Now, how do you get around this?

One option is to make the class invariant (drop the +). Another option is that if you take a method parameter, to allow any superclass as well: m[S >: T](s: S). Now if T changes to U, it's no big deal: a superclass of T is also a superclass of U, and the method will work. (However, you then have to change your method to be able to handle such things.)

With a case class, it's even harder to get it right unless you make it invariant. I recommend doing that, and pushing the generics and variance elsewhere. But I'd need to see more details to be sure that this would work for your use case.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • thank you for your answer. Your solution however does not work for me in this case. Dropping covariance and making the trait invariant would work but it's not what i want here. Allowing the method (or in my case the case-class) to take a supertype is also not satisfying. I'm curious to why things are harder to get right for case-classes. Note that the same code without the _case_ keyword works just fine. – lapislazuli Mar 08 '12 at 16:50
  • 2
    @lapislazuli - Because case classes include a companion method that creates them (taking `T` as an argument), so you have to obey the method restrictions above. If you don't include `case`, then the class doesn't imply a method in the interface that takes `T`s. – Rex Kerr Mar 08 '12 at 16:57
  • 1
    What's the reason that, in `class F[+A] { def f(x: A) = ??? }`, that `x` is in `contravariant position`? Is it due to the `contravariance` of `T` in [Function[-T1, +R](https://github.com/scala/scala/blob/v2.11.8/src/library/scala/Function1.scala#L32)? – Kevin Meredith Jul 06 '16 at 02:35
13

Almost there. Here:

scala> trait MyTrait[+T] {
     |   private case class MyClass[U >: T](c: U)
     | }
defined trait MyTrait

Which means MyClass[Any] is valid for all T. That is at the root of why one cannot use T in that position, but demonstrating it requires more code than I'm in the mood for at the moment. :-)

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681