Skip to content

Commit 11c6ca3

Browse files
flomebulbishabosha
andauthored
Add code tabs for _tour/variances (#2547)
* Add code tabs for _tour/variances * Update _tour/variances.md Co-authored-by: Jamie Thompson <[email protected]> * Update _tour/variances.md Co-authored-by: Jamie Thompson <[email protected]> * Update variances.md In fact, this code fails at compile time... Co-authored-by: Jamie Thompson <[email protected]>
1 parent b86a56a commit 11c6ca3

File tree

1 file changed

+90
-15
lines changed

1 file changed

+90
-15
lines changed

Diff for: _tour/variances.md

+90-15
Original file line numberDiff line numberDiff line change
@@ -12,55 +12,99 @@ redirect_from: "/tutorials/tour/variances.html"
1212

1313
Variance lets you control how type parameters behave with regards to subtyping. Scala supports variance annotations of type parameters of [generic classes](generic-classes.html), to allow them to be covariant, contravariant, or invariant if no annotations are used. The use of variance in the type system allows us to make intuitive connections between complex types.
1414

15+
{% tabs variances_1 %}
16+
{% tab 'Scala 2 and 3' for=variances_1 %}
1517
```scala mdoc
1618
class Foo[+A] // A covariant class
1719
class Bar[-A] // A contravariant class
1820
class Baz[A] // An invariant class
1921
```
22+
{% endtab %}
23+
{% endtabs %}
2024

2125
### Invariance
2226

2327
By default, type parameters in Scala are invariant: subtyping relationships between the type parameters aren't reflected in the parameterized type. To explore why this works the way it does, we look at a simple parameterized type, the mutable box.
2428

29+
{% tabs invariance_1 %}
30+
{% tab 'Scala 2 and 3' for=invariance_1 %}
2531
```scala mdoc
2632
class Box[A](var content: A)
2733
```
34+
{% endtab %}
35+
{% endtabs %}
2836

2937
We're going to be putting values of type `Animal` in it. This type is defined as follows:
3038

39+
{% tabs invariance_2 class=tabs-scala-version %}
40+
{% tab 'Scala 2' for=invariance_2 %}
3141
```scala mdoc
3242
abstract class Animal {
3343
def name: String
3444
}
3545
case class Cat(name: String) extends Animal
3646
case class Dog(name: String) extends Animal
3747
```
48+
{% endtab %}
49+
{% tab 'Scala 3' for=invariance_2 %}
50+
```scala
51+
abstract class Animal:
52+
def name: String
53+
54+
case class Cat(name: String) extends Animal
55+
case class Dog(name: String) extends Animal
56+
```
57+
{% endtab %}
58+
{% endtabs %}
3859

3960
We can say that `Cat` is a subtype of `Animal`, and that `Dog` is also a subtype of `Animal`. That means that the following is well-typed:
4061

62+
{% tabs invariance_3 %}
63+
{% tab 'Scala 2 and 3' for=invariance_3 %}
4164
```scala mdoc
42-
val myAnimal: Animal = Cat("Felix")
65+
val myAnimal: Animal = Cat("Felix")
4366
```
67+
{% endtab %}
68+
{% endtabs %}
4469

4570
What about boxes? Is `Box[Cat]` a subtype of `Box[Animal]`, like `Cat` is a subtype of `Animal`? At first sight, it looks like that may be plausible, but if we try to do that, the compiler will tell us we have an error:
4671

72+
{% tabs invariance_4 class=tabs-scala-version %}
73+
{% tab 'Scala 2' for=invariance_4 %}
74+
```scala mdoc:fail
75+
val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
76+
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
77+
val myAnimal: Animal = myAnimalBox.content
78+
```
79+
{% endtab %}
80+
{% tab 'Scala 3' for=invariance_4 %}
4781
```scala
48-
val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix"))
49-
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
50-
val myAnimal: Animal = myAnimalBox.content
82+
val myCatBox: Box[Cat] = Box[Cat](Cat("Felix"))
83+
val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile
84+
val myAnimal: Animal = myAnimalBox.content
5185
```
86+
{% endtab %}
87+
{% endtabs %}
5288

5389
Why could this be a problem? We can get the cat from the box, and it's still an Animal, isn't it? Well, yes. But that's not all we can do. We can also replace the cat in the box with a different animal
5490

91+
{% tabs invariance_5 %}
92+
{% tab 'Scala 2 and 3' for=invariance_5 %}
5593
```scala
5694
myAnimalBox.content = Dog("Fido")
5795
```
96+
{% endtab %}
97+
{% endtabs %}
5898

5999
There now is a Dog in the Animal box. That's all fine, you can put Dogs in Animal boxes, because Dogs are Animals. But our Animal Box is a Cat Box! You can't put a Dog in a Cat box. If we could, and then try to get the cat from our Cat Box, it would turn out to be a dog, breaking type soundness.
60100

101+
{% tabs invariance_6 %}
102+
{% tab 'Scala 2 and 3' for=invariance_6 %}
61103
```scala
62104
val myCat: Cat = myCatBox.content //myCat would be Fido the dog!
63105
```
106+
{% endtab %}
107+
{% endtabs %}
64108

65109
From this, we have to conclude that `Box[Cat]` and `Box[Animal]` can't have a subtyping relationship, even though `Cat` and `Animal` do.
66110

@@ -70,18 +114,31 @@ The problem we ran in to above, is that because we could put a Dog in an Animal
70114

71115
But what if we couldn't put a Dog in the box? Then we could just get our Cat back out and that's not a problem, so than it could follow the subtyping relationship. It turns out, that's indeed something we can do.
72116

117+
{% tabs covariance_1 class=tabs-scala-version %}
118+
{% tab 'Scala 2' for=covariance_1 %}
73119
```scala mdoc
74-
class ImmutableBox[+A](val content: A)
75-
val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
76-
val animalBox: ImmutableBox[Animal] = catbox // now this compiles
120+
class ImmutableBox[+A](val content: A)
121+
val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix"))
122+
val animalBox: ImmutableBox[Animal] = catbox // now this compiles
123+
```
124+
{% endtab %}
125+
{% tab 'Scala 3' for=covariance_1 %}
126+
```scala
127+
class ImmutableBox[+A](val content: A)
128+
val catbox: ImmutableBox[Cat] = ImmutableBox[Cat](Cat("Felix"))
129+
val animalBox: ImmutableBox[Animal] = catbox // now this compiles
77130
```
131+
{% endtab %}
132+
{% endtabs %}
78133

79134
We say that `ImmutableBox` is *covariant* in `A`, and this is indicated by the `+` before the `A`.
80135

81136
More formally, that gives us the following relationship: given some `class Cov[+T]`, then if `A` is a subtype of `B`, `Cov[A]` is a subtype of `Cov[B]`. This allows us to make very useful and intuitive subtyping relationships using generics.
82137

83138
In the following less contrived example, the method `printAnimalNames` will accept a list of animals as an argument and print their names each on a new line. If `List[A]` were not covariant, the last two method calls would not compile, which would severely limit the usefulness of the `printAnimalNames` method.
84139

140+
{% tabs covariance_2 %}
141+
{% tab 'Scala 2 and 3' for=covariance_2 %}
85142
```scala mdoc
86143
def printAnimalNames(animals: List[Animal]): Unit =
87144
animals.foreach {
@@ -97,22 +154,40 @@ printAnimalNames(cats)
97154
// prints: Fido, Rex
98155
printAnimalNames(dogs)
99156
```
157+
{% endtab %}
158+
{% endtabs %}
100159

101160
### Contravariance
102161

103162
We've seen we can accomplish covariance by making sure that we can't put something in the covariant type, but only get something out. What if we had the opposite, something you can put something in, but can't take out? This situation arises if we have something like a serializer, that takes values of type A, and converts them to a serialized format.
104163

164+
{% tabs contravariance_1 class=tabs-scala-version %}
165+
{% tab 'Scala 2' for=contravariance_1 %}
105166
```scala mdoc
106-
abstract class Serializer[-A] {
107-
def serialize(a: A): String
108-
}
167+
abstract class Serializer[-A] {
168+
def serialize(a: A): String
169+
}
109170

110-
val animalSerializer: Serializer[Animal] = new Serializer[Animal] {
111-
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
112-
}
113-
val catSerializer: Serializer[Cat] = animalSerializer
114-
catSerializer.serialize(Cat("Felix"))
171+
val animalSerializer: Serializer[Animal] = new Serializer[Animal] {
172+
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
173+
}
174+
val catSerializer: Serializer[Cat] = animalSerializer
175+
catSerializer.serialize(Cat("Felix"))
176+
```
177+
{% endtab %}
178+
{% tab 'Scala 3' for=contravariance_1 %}
179+
```scala
180+
abstract class Serializer[-A]:
181+
def serialize(a: A): String
182+
183+
val animalSerializer: Serializer[Animal] = Serializer[Animal]():
184+
def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }"""
185+
186+
val catSerializer: Serializer[Cat] = animalSerializer
187+
catSerializer.serialize(Cat("Felix"))
115188
```
189+
{% endtab %}
190+
{% endtabs %}
116191

117192
We say that `Serializer` is *contravariant* in `A`, and this is indicated by the `-` before the `A`. A more general serializer is a subtype of a more specific serializer.
118193

0 commit comments

Comments
 (0)