Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics invariant covariant contravariant in scala

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.

like image 500
vijayinani Avatar asked May 12 '18 05:05

vijayinani


People also ask

What is covariance and Contravariance in Scala?

Covariance allows assigning an instance to a variable whose type is one of the instance's generic type; i.e. supertype. Contravariance allows assigning an instance to a variable whose type is one of the instance's derived type; i.e. subtype.

What is covariance and Contravariance in generics?

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.

How do I use generics in Scala?

The symbols used for a type parameter of second, third, fourth, and so on, types in generic classes are respectively B, C, D, and so on. The symbol used for a key is A and for a value is B in Scala Map. The symbol used for a numeric value is N.

What are type parameters in Scala?

Methods in Scala can be parameterized by type as well as value. The syntax is similar to that of generic classes. Type parameters are enclosed in square brackets, while value parameters are enclosed in parentheses.


1 Answers

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.

like image 159
Jörg W Mittag Avatar answered Oct 05 '22 01:10

Jörg W Mittag