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.
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.
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.
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.
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.
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:
A
.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:
A <: B
⇒ C[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.A <: B
⇒ C[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.C[A]
and C[B]
, neither is a sub- nor supertype of the other.There are two questions you might ask yourself now:
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 Fruit
s to Mammal
s (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 Apple
s to Mammal
s 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 Apple
s doesn't know how to deal with Orange
s, 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 Orange
s. 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:
C[A]
covariant in A
IFF A
is used only as an output.C[A]
contravariant in A
IFF A
is used only as an input.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With