Following on from this question, can someone explain the following in Scala:
class Slot[+T] (var some: T) { // DOES NOT COMPILE // 'COVARIANT parameter in CONTRAVARIANT position' }
I understand the distinction between +T and T in the type declaration (it compiles if I use T). But then how does one actually write a class which is covariant in its type parameter without resorting to creating the thing unparametrized? How can I ensure that the following can only be created with an instance of T?
class Slot[+T] (var some: Object){ def get() = { some.asInstanceOf[T] } }
EDIT – now got this down to the following:
abstract class _Slot[+T, V <: T] (var some: V) { def getT() = { some } }
this is all good, but I now have two type parameters, where I only want one. I’ll re-ask the question thus:
How can I write an immutable Slot class which is covariant in its type?
EDIT 2: Duh! I used var and not val. The following is what I wanted:
class Slot[+T] (val some: T) { }
Generically, a covariant type parameter is one which is allowed to vary down as the class is subtyped (alternatively, vary with subtyping, hence the ‘co-‘ prefix). More concretely:
List[Int]is a subtype ofList[AnyVal]becauseIntis a subtype ofAnyVal. This means that you may provide an instance ofList[Int]when a value of typeList[AnyVal]is expected. This is really a very intuitive way for generics to work, but it turns out that it is unsound (breaks the type system) when used in the presence of mutable data. This is why generics are invariant in Java. Brief example of unsoundness using Java arrays (which are erroneously covariant):We just assigned a value of type
Stringto an array of typeInteger[]. For reasons which should be obvious, this is bad news. Java’s type system actually allows this at compile time. The JVM will ‘helpfully’ throw anArrayStoreExceptionat runtime. Scala’s type system prevents this problem because the type parameter on theArrayclass is invariant (declaration is[A]rather than[+A]).Note that there is another type of variance known as contravariance. This is very important as it explains why covariance can cause some issues. Contravariance is literally the opposite of covariance: parameters vary upward with subtyping. It is a lot less common partially because it is so counter-intuitive, though it does have one very important application: functions.
Notice the ‘–‘ variance annotation on the
Ptype parameter. This declaration as a whole means thatFunction1is contravariant inPand covariant inR. Thus, we can derive the following axioms:Notice that
T1'must be a subtype (or the same type) ofT1, whereas it is the opposite forT2andT2'. In English, this can be read as the following:The reason for this rule is left as an exercise to the reader (hint: think about different cases as functions are subtyped, like my array example from above).
With your new-found knowledge of co- and contravariance, you should be able to see why the following example will not compile:
The problem is that
Ais covariant, while theconsfunction expects its type parameter to be invariant. Thus,Ais varying the wrong direction. Interestingly enough, we could solve this problem by makingListcontravariant inA, but then the return typeList[A]would be invalid as theconsfunction expects its return type to be covariant.Our only two options here are to a) make
Ainvariant, losing the nice, intuitive sub-typing properties of covariance, or b) add a local type parameter to theconsmethod which definesAas a lower bound:This is now valid. You can imagine that
Ais varying downward, butBis able to vary upward with respect toAsinceAis its lower-bound. With this method declaration, we can haveAbe covariant and everything works out.Notice that this trick only works if we return an instance of
Listwhich is specialized on the less-specific typeB. If you try to makeListmutable, things break down since you end up trying to assign values of typeBto a variable of typeA, which is disallowed by the compiler. Whenever you have mutability, you need to have a mutator of some sort, which requires a method parameter of a certain type, which (together with the accessor) implies invariance. Covariance works with immutable data since the only possible operation is an accessor, which may be given a covariant return type.