Your class can be viewed as
trait Function1[-Input, +Result] // Renamed the type parameters for clarity
case class Foo[+A](a: A) {
val bar: Function1[A, A] = identity
val zar: Function1[Function1[A, Int], Int] = { f => f(a) }
}
The approach taken by the compiler is to assign positive, negative, and neutral annotations at each type position in the signature; I'll notate the positions by putting + and - after the type in that position
First the top-level val
s are positive (i.e. covariant):
case class Foo[+A](a: A) {
val bar: Function1[A, A]+
val zar: Function1[Function1[A, Int], Int]+
}
bar
can be anything that's a subtype of Function1[A, A]
and zar
can be anything that's a subtype of Function1[Function1[A, Int], Int]
, so this makes sense (LSP, etc.).
The compiler then moves into the type arguments.
case class Foo[+A](a: A) {
val bar: Function1[A-, A+]
val zar: Function1[Function1[A, Int]-, Int+]
}
Since Input
is contravariant, this "flips" the classification (+ -> -, - -> +, neutral unchanged) relative to its surrounding classification. Result
being covariant does not flip the classification (if there was an invariant parameter in Function1
, that would force the classification to neutral). Applying this a second time
case class Foo[+A](a: A) {
val bar: Function1[A-, A+]
val zar: Function1[Function1[A+, Int-], Int+]
}
A type parameter for the class being defined can only be used in + position if it's covariant, - position if contravariant, and anywhere if it's invariant (Int
, which is not a type parameter can be considered invariant for the purpose of this analysis: i.e. we could have dispensed with annotating once we got to a type which neither was a type parameter nor had a type parameter). In bar
, we have a conflict (the A-
), but in zar
the A+
means no conflict.
The produce/consume relationship presented in Luis's answer is a good intuitive summary. This is a partial (methods with type parameters complicate this, though I'm not totally sure how my transformation would work there...) exploration of how the compiler actually concludes that the A
in zar
is in a covariant position; Programming in Scala (Odersky, Spoon, Venners) describes it in more detail (in the 3rd edition, it's in section 19.4).