From 50b895644c4146538f3903af0c9c5689c9df25e0 Mon Sep 17 00:00:00 2001 From: Luc Henninger Date: Wed, 21 Sep 2022 16:01:24 +0200 Subject: [PATCH 1/4] Add code tabs for _tour/variances --- _tour/variances.md | 105 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/_tour/variances.md b/_tour/variances.md index 3dc67cf942..6b07592a50 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -12,22 +12,32 @@ redirect_from: "/tutorials/tour/variances.html" 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. +{% tabs variances_1 %} +{% tab 'Scala 2 and 3' for=variances_1 %} ```scala mdoc class Foo[+A] // A covariant class class Bar[-A] // A contravariant class class Baz[A] // An invariant class ``` +{% endtab %} +{% endtabs %} ### Invariance 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. +{% tabs invariance_1 %} +{% tab 'Scala 2 and 3' for=invariance_1 %} ```scala mdoc class Box[A](var content: A) ``` +{% endtab %} +{% endtabs %} We're going to be putting values of type `Animal` in it. This type is defined as follows: +{% tabs invariance_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=invariance_2 %} ```scala mdoc abstract class Animal { def name: String @@ -35,32 +45,66 @@ abstract class Animal { case class Cat(name: String) extends Animal case class Dog(name: String) extends Animal ``` +{% endtab %} +{% tab 'Scala 3' for=invariance_2 %} +```scala +abstract class Animal: + def name: String + +case class Cat(name: String) extends Animal +case class Dog(name: String) extends Animal +``` +{% endtab %} +{% endtabs %} 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: +{% tabs invariance_3 %} +{% tab 'Scala 2 and 3' for=invariance_3 %} ```scala mdoc - val myAnimal: Animal = Cat("Felix") +val myAnimal: Animal = Cat("Felix") ``` +{% endtab %} +{% endtabs %} 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: +{% tabs invariance_4 class=tabs-scala-version %} +{% tab 'Scala 2' for=invariance_4 %} +```scala +val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix")) +val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile +val myAnimal: Animal = myAnimalBox.content +``` +{% endtab %} +{% tab 'Scala 3' for=invariance_4 %} ```scala - val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix")) - val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile - val myAnimal: Animal = myAnimalBox.content +val myCatBox: Box[Cat] = Box[Cat](Cat("Felix")) +val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile +val myAnimal_1: Animal = myAnimalBox.content ``` +{% endtab %} +{% endtabs %} 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 +{% tabs invariance_5 %} +{% tab 'Scala 2 and 3' for=invariance_5 %} ```scala myAnimalBox.content = Dog("Fido") ``` +{% endtab %} +{% endtabs %} 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. +{% tabs invariance_6 %} +{% tab 'Scala 2 and 3' for=invariance_6 %} ```scala val myCat: Cat = myCatBox.content //myCat would be Fido the dog! ``` +{% endtab %} +{% endtabs %} From this, we have to conclude that `Box[Cat]` and `Box[Animal]` can't have a subtyping relationship, even though `Cat` and `Animal` do. @@ -70,11 +114,22 @@ The problem we ran in to above, is that because we could put a Dog in an Animal 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. +{% tabs covariance_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=covariance_1 %} ```scala mdoc - class ImmutableBox[+A](val content: A) - val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix")) - val animalBox: ImmutableBox[Animal] = catbox // now this compiles +class ImmutableBox[+A](val content: A) +val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix")) +val animalBox: ImmutableBox[Animal] = catbox // now this compiles +``` +{% endtab %} +{% tab 'Scala 3' for=covariance_1 %} +```scala +class ImmutableBox[+A](val content: A) +val catbox: ImmutableBox[Cat] = ImmutableBox[Cat](Cat("Felix")) +val animalBox: ImmutableBox[Animal] = catbox // now this compiles ``` +{% endtab %} +{% endtabs %} We say that `ImmutableBox` is *covariant* in `A`, and this is indicated by the `+` before the `A`. @@ -82,6 +137,8 @@ More formally, that gives us the following relationship: given some `class Cov[+ 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. +{% tabs covariance_2 %} +{% tab 'Scala 2 and 3' for=covariance_2 %} ```scala mdoc def printAnimalNames(animals: List[Animal]): Unit = animals.foreach { @@ -97,22 +154,40 @@ printAnimalNames(cats) // prints: Fido, Rex printAnimalNames(dogs) ``` +{% endtab %} +{% endtabs %} ### Contravariance 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. +{% tabs contravariance_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=contravariance_1 %} ```scala mdoc - abstract class Serializer[-A] { - def serialize(a: A): String - } +abstract class Serializer[-A] { + def serialize(a: A): String +} - val animalSerializer: Serializer[Animal] = new Serializer[Animal] { - def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }""" - } - val catSerializer: Serializer[Cat] = animalSerializer - catSerializer.serialize(Cat("Felix")) +val animalSerializer: Serializer[Animal] = new Serializer[Animal] { + def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }""" +} +val catSerializer: Serializer[Cat] = animalSerializer +catSerializer.serialize(Cat("Felix")) +``` +{% endtab %} +{% tab 'Scala 3' for=contravariance_1 %} +```scala +abstract class Serializer[-A]: + def serialize(a: A): String + +val animalSerializer: Serializer[Animal] = Serializer[Animal](): + def serialize(animal: Animal): String = s"""{ "name": "${animal.name}" }""" + +val catSerializer: Serializer[Cat] = animalSerializer +catSerializer.serialize(Cat("Felix")) ``` +{% endtab %} +{% endtabs %} 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. From 4b6679215514b49a12c70d6741d7b34ea88fd64c Mon Sep 17 00:00:00 2001 From: Luc Henninger Date: Wed, 21 Sep 2022 18:59:01 +0200 Subject: [PATCH 2/4] Update _tour/variances.md Co-authored-by: Jamie Thompson --- _tour/variances.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tour/variances.md b/_tour/variances.md index 6b07592a50..3f7a08219d 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -81,7 +81,7 @@ val myAnimal: Animal = myAnimalBox.content ```scala val myCatBox: Box[Cat] = Box[Cat](Cat("Felix")) val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile -val myAnimal_1: Animal = myAnimalBox.content +val myAnimal: Animal = myAnimalBox.content ``` {% endtab %} {% endtabs %} From 204768e81729e1beb15f38d2ccdf2d409f8741d8 Mon Sep 17 00:00:00 2001 From: Luc Henninger Date: Wed, 21 Sep 2022 19:01:35 +0200 Subject: [PATCH 3/4] Update _tour/variances.md Co-authored-by: Jamie Thompson --- _tour/variances.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tour/variances.md b/_tour/variances.md index 3f7a08219d..4b91be034a 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -71,7 +71,7 @@ What about boxes? Is `Box[Cat]` a subtype of `Box[Animal]`, like `Cat` is a subt {% tabs invariance_4 class=tabs-scala-version %} {% tab 'Scala 2' for=invariance_4 %} -```scala +```scala mdoc:nest val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix")) val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile val myAnimal: Animal = myAnimalBox.content From d17e7d48dc3904059a24d53c022666a5d5aee432 Mon Sep 17 00:00:00 2001 From: Luc Henninger Date: Wed, 21 Sep 2022 19:10:58 +0200 Subject: [PATCH 4/4] Update variances.md In fact, this code fails at compile time... --- _tour/variances.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tour/variances.md b/_tour/variances.md index 4b91be034a..eab3d63682 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -71,7 +71,7 @@ What about boxes? Is `Box[Cat]` a subtype of `Box[Animal]`, like `Cat` is a subt {% tabs invariance_4 class=tabs-scala-version %} {% tab 'Scala 2' for=invariance_4 %} -```scala mdoc:nest +```scala mdoc:fail val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix")) val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile val myAnimal: Animal = myAnimalBox.content