5
votes

This could be a very silly question, but I am not able to understand the difference even after scratching my head for a long time.

I am going through the page of scala generics: https://docs.scala-lang.org/tour/generic-classes.html

Here, it is said that

Note: subtyping of generic types is invariant. This means that if we have a stack of characters of type Stack[Char] then it cannot be used as an integer stack of type Stack[Int]. This would be unsound because it would enable us to enter true integers into the character stack. To conclude, Stack[A] is only a subtype of Stack[B] if and only if B = A.

I understand this completely that I cannot use Char where Int is required. But, my Stack class accepts only A type (which is invariant). If I put Apple, Banana or Fruit in them, they all are accepted.

class Fruit

class Apple extends Fruit

class Banana extends Fruit

  val stack2 = new Stack[Fruit]
  stack2.push(new Fruit)
  stack2.push(new Banana)
  stack2.push(new Apple)

But, on the next page (https://docs.scala-lang.org/tour/variances.html), it says that type parameter should be covariant +A, then how is the Fruit example working as even it is adding the subtypes with invariant.

Hope I am clear with my question. Let me know if more Info. needs to be added.

3
didn't you read Note: subtyping of generic types is invariant.?Ramesh Maharjan
You might find this illustration useful (it worked even for Java programmers).Andrey Tyukin

3 Answers

6
votes

This has nothing to do with variance at all.

You declare stack2 to be a Stack[Fruit], in other words, you declare that you are allowed to put anything into the Stack which is a Fruit. An Apple is a (subtype of) Fruit, ergo you are allowed to put an Apple into a Stack of Fruits.

This is called subtyping and has nothing to do with variance at all.

Let's take a step back: what does variance actually mean?

Well, variance means "change" (think of words like "to vary" or "variable"). co- means "together" (think of cooperation, co-education, co-location), contra- means "against" (think of contradiction, counter-intelligence, counter-insurgency, contraceptive), and in- means "unrelated" or "non-" (think of involuntary, inaccessible, intolerant).

So, we have "change" and that change can be "together", "against" or "unrelated". Well, in order to have related changes, we need two things which change, and they can either change together (i.e. when one thing changes, the other thing also changes "in the same direction"), they can change against each other (i.e. when one thing changes, the other thing changes "in the opposite direction"), or they can be unrelated (i.e. when one thing changes, the other doesn't.)

And that's all there is to the mathematical concept of covariance, contravariance, and invariance. All we need are two "things", some notion of "change", and this change needs to have some notion of "direction".

Now, that's of course very abstract. In this particular instance, we are talking about the context of subtyping and parametric polymorphism. How does this apply here?

Well, what are our two things? When we have a type constructor such as C[A], then our two things are:

  1. The type argument A.
  2. The constructed type which is the result of applying the type constructor C to A.

And what is our change with a sense of direction? It is subtyping!

So, the question now becomes: "When I change A to B (along one of the directions of subtyping, i.e. make it either a subtype or a supertype), then how does C[A] relate to C[B]".

And again, there are three possibilities:

  • Covariance: A <: BC[A] <: C[B]: when A is a subtype of B then C[A] is a subtype of C[B], in other words, when I change A along the subtyping hierarchy, then C[A] changes with A in the same direction.
  • Contravariance: A <: BC[A] :> C[B]: when A is a subtype of B, then C[A] is a supertype of C[B], in other words, when I change A along the subtyping hierarchy, then C[A] changes against A in the opposite direction.
  • Invariance: there is no subtyping relationship between C[A] and C[B], neither is a sub- nor supertype of the other.

There are two questions you might ask yourself now:

  1. Why is this useful?
  2. Which one is the right one?

This is useful for the same reason subtyping is useful. In fact, this is just subtyping. So, if you have a language which has both subtyping and parametric polymorphism, then it is important to know whether one type is a subtype of another type, and variance tells you whether or not a constructed type is a subtype of another constructed type of the same constructor based on the subtyping relationship between the type arguments.

Which one is the right one is trickier, but thankfully, we have a powerful tool for analyzing when a subtype is a subtype of another type: Barbara Liskov's Substitution Principle tells us that a type S is a subtype of type T IFF any instance of T can be replaced with an instance of S without changing the observable desirable properties of the program.

Let's take a simple generic type, a function. A function has two type parameters, one for the input, and one for the output. (We are keeping it simple here.) F[A, B] is a function that takes in an argument of type A and returns a result of type B.

And now we play through a couple of scenarios. I have some operation O that wants to work with a function from Fruits to Mammals (yeah, I know, exciting original examples!) The LSP says that I should also be able to pass in a subtype of that function, and everything should still work. Let's say, F were covariant in A. Then I should be able to pass in a function from Apples to Mammals as well. But what happens when O passes an Orange to F? That should be allowed! O was able to pass an Orange to F[Fruit, Mammal] because Orange is a subtype of Fruit. But, a function from Apples doesn't know how to deal with Oranges, so it blows up. The LSP says it should work though, which means that the only conclusion we can draw is that our assumption is wrong: F[Apple, Mammal] is not a subtype of F[Fruit, Mammal], in other words, F is not covariant in A.

What if it were contravariant? What if we pass an F[Food, Mammal] into O? Well, O again tries to pass an Orange and it works: Orange is a Food, so F[Food, Mammal] knows how to deal with Oranges. We can now conclude that functions are contravariant in their inputs, i.e. you can pass a function that takes a more general type as its input as a replacement for a function that takes a more restricted type and everything will work out fine.

Now let's look at the output of F. What would happen if F were contravariant in B just like it is in A? We pass an F[Fruit, Animal] to O. According to the LSP, if we are right and functions are contravariant in their output, nothing bad should happen. Unfortunately, O calls the getMilk method on the result of F, but F just returned it a Chicken. Oops. Ergo, functions can't be contravariant in their outputs.

OTOH, what happens if we pass an F[Fruit, Cow]? Everything still works! O calls getMilk on the returned cow, and it indeed gives milk. So, it looks like functions are covariant in their outputs.

And that is a general rule that applies to variance:

  • It is safe (in the sense of the LSP) to make C[A] covariant in A IFF A is used only as an output.
  • It is safe (in the sense of the LSP) to make C[A] contravariant in A IFF A is used only as an input.
  • If A can be used either as an input or as an output, then C[A] must be invariant in A, otherwise the result is not safe.

In fact, that's why C♯'s designers chose to re-use the already existing keywords in and out for variance annotations and Kotlin uses those same keywords.

So, for example, immutable collections can generally be covariant in their element type, since they don't allow you to put something into the collection (you can only construct a new collection with a potentially different type) but only to get elements out. So, if I want to get a list of numbers, and someone hands me a list of integers, I am fine.

On the other hand, think of an output stream (such as a Logger), where you can only put stuff in but not get it out. For this, it is safe to be contravariant. I.e. if I expect to be able to print strings, and someone hands me a printer that can print any object, then it can also print strings, and I am fine. Other examples are comparison functions (you only put generics in, the output is fixed to be a boolean or an enum or an integer or whatever design your particular language chooses). Or predicates, they only have generic inputs, the output is always fixed to be a boolean.

But, for example, mutable collections, where you can both put stuff in and get stuff out, are only type-safe when they are invariant. There are a great many tutorials explaining in detail how to break Java's or C♯'s type-safety using their covariant mutable arrays, for example.

Note, however that it is not always obvious whether a type is an input or an output once you get to more complex types. For example, when your type parameter is used as the upper or lower bound of an abstract type member, or when you have a method which takes a function that returns a function whose argument type is your type parameter.

Now, to come back to your question: you only have one stack. You never ask whether one stack is a subtype of another stack. Therefore, variance doesn't come into play in your example.

3
votes

One of the non-obvious things about Scala type variance is that the annotation, +A and -A, actually tells us more about the wrapper than it does about the type parameter.

Let's say you have a box: class Box[T]

Because T is invariant that means that some Box[Apple] is unrelated to a Box[Fruit].

Now let's make it covariant: class Box[+T]

This does two things, it restricts the way the Box code can use T internally, but, more importantly, it changes the relationship between various instances of Boxes. In particular, the type Box[Apple] is now a sub-type of Box[Fruit], because Apple is a sub-type of Fruit, and we've instructed Box to vary its type relationships in the same manner (i.e. "co-") as its type parameter.

... it says that type parameter should be covariant +A

Actually, that Stack code can't be made co- or contra-variant. As I mentioned, variance annotation adds some restrictions to the way the type parameter is used and that Stack code uses A in ways that are contrary to both co- and contra-variance.

-1
votes

Variance is related more with complex type rather then passing objects which is called subtyping.

Explained here:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29

If you want to make a complex type that accepts some type as a child/parent of list that accepts certain other type, then idea of variance comes int effect. As in your example, it is about passing child in place of parent. So it works.

https://coderwall.com/p/dlqvnq/simple-example-for-scala-covariance-contravariance-and-invariance

Please see the code here. It is understandable. Please respond if you do not get it.