From 61f7ee903bac4c29188588eee78eaa70e74c93ea Mon Sep 17 00:00:00 2001 From: Martijn Hoekstra Date: Fri, 24 Jun 2022 15:54:27 +0200 Subject: [PATCH 1/3] Update variances.md Mostly a re-write. * Begin with invariance, since it's the default without any annotations * Move to covariance, which is the thing that's most intuitive for most people * Introduce contravariance as the opposite of covariance * Remove more contrived examples. Less is more --- _tour/variances.md | 134 ++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 75 deletions(-) diff --git a/_tour/variances.md b/_tour/variances.md index bb685dcd96..3a68b4ff60 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -10,7 +10,7 @@ previous-page: generic-classes redirect_from: "/tutorials/tour/variances.html" --- -Variance is the correlation of subtyping relationships of complex types and the subtyping relationships of their component types. 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, whereas the lack of variance can restrict the reuse of a class abstraction. +Variance is a property of parameterized types that relates the subtyping relationship of parameterized types to the subtyping releationship of its type parameter. 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. ```scala mdoc class Foo[+A] // A covariant class @@ -18,11 +18,15 @@ class Bar[-A] // A contravariant class class Baz[A] // An invariant class ``` -### Covariance +### Invariance -A type parameter `T` of a generic class can be made covariant by using the annotation `+T`. For some `class List[+T]`, making `T` covariant implies that for two types `A` and `B` where `B` is a subtype of `A`, then `List[B]` is a subtype of `List[A]`. This allows us to make very useful and intuitive subtyping relationships using generics. +By default, type paramters in scala are invariant: subtyping relationships between the type paramter 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. -Consider this simple class structure: +```scala mdoc +class Box[A](var content: A) +``` + +We're going to be putting values of type `Animal` in it. This type is defined as follows: ```scala mdoc abstract class Animal { @@ -32,110 +36,90 @@ case class Cat(name: String) extends Animal case class Dog(name: String) extends Animal ``` -Both `Cat` and `Dog` are subtypes of `Animal`. The Scala standard library has a generic immutable `sealed abstract class List[+A]` class, where the type parameter `A` is covariant. This means that a `List[Cat]` is a `List[Animal]`. A `List[Dog]` is also a `List[Animal]`. Intuitively, it makes sense that a list of cats and a list of dogs are each lists of animals, and you should be able to use either of them for in place of `List[Animal]`. - -In the following 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. +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: ```scala mdoc -def printAnimalNames(animals: List[Animal]): Unit = - animals.foreach { - animal => println(animal.name) - } + val myAnimal: Animal = Cat("felix") +``` -val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom")) -val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex")) +What about boxes? If I have a `Box[Cat]`, is it 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: -// prints: Whiskers, Tom -printAnimalNames(cats) - -// prints: Fido, Rex -printAnimalNames(dogs) +```scala + val myCatBox: Box[Cat] = new Box[Cat](Cat("felix")) + val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile + val myAnimal: Animal = myAnimalBox.content ``` -### Contravariance +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 -A type parameter `A` of a generic class can be made contravariant by using the annotation `-A`. This creates a subtyping relationship between the class and its type parameter that is similar, but opposite to what we get with covariance. That is, for some `class Printer[-A]`, making `A` contravariant implies that for two types `A` and `B` where `A` is a subtype of `B`, `Printer[B]` is a subtype of `Printer[A]`. +```scala + mayAnimalBox.content = Dog("fido") +``` -Consider the `Cat`, `Dog`, and `Animal` classes defined above for the following example: +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. Doing so would not only be unpleasant for the dog, if we try to get the cat from our catbox, it would turn out to be a dog, breaking type soundness -```scala mdoc -abstract class Printer[-A] { - def print(value: A): Unit -} +```scala + val myCat: Cat = myCatBox.content //myCat would be fido the dog! ``` -A `Printer[A]` is a simple class that knows how to print out some type `A`. Let's define some subclasses for specific types: +From this, we have to conclude that `Box[Cat]` and `Box[Animal]` can't have a subtyping relationship, even though `Cat` and `Animal` do. -```scala mdoc -class AnimalPrinter extends Printer[Animal] { - def print(animal: Animal): Unit = - println("The animal's name is: " + animal.name) -} +### Covariance -class CatPrinter extends Printer[Cat] { - def print(cat: Cat): Unit = - println("The cat's name is: " + cat.name) -} -``` +The problem we ran in to above, is that because we could put a Dog in an Animal Box, a Cat Box can't be an Animal Box. -If a `Printer[Cat]` knows how to print any `Cat` to the console, and a `Printer[Animal]` knows how to print any `Animal` to the console, it makes sense that a `Printer[Animal]` would also know how to print any `Cat`. The inverse relationship does not apply, because a `Printer[Cat]` does not know how to print any `Animal` to the console. Therefore, we should be able to use a `Printer[Animal]` in place of `Printer[Cat]`, if we wish, and making `Printer[A]` contravariant allows us to do exactly that. +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. ```scala mdoc -def printMyCat(printer: Printer[Cat], cat: Cat): Unit = - printer.print(cat) - -val catPrinter: Printer[Cat] = new CatPrinter -val animalPrinter: Printer[Animal] = new AnimalPrinter - -printMyCat(catPrinter, Cat("Boots")) -printMyCat(animalPrinter, Cat("Boots")) + class ImmutableBox[+A](val content: A) + val catbox: ImmutableBox[Cat] = new ImmutableBox[Cat](Cat("Felix")) + val animalBox: ImmutableBox[Animal] = catbox // now this compiles ``` -The output of this program will be: +We say that `ImmutableBox` is *covariant* in `A`, and this is indicated by the `+` before the `A`. -``` -The cat's name is: Boots -The animal's name is: Boots -``` +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. -### Invariance - -Generic classes in Scala are invariant by default. This means that they are neither covariant nor contravariant. In the context of the following example, `Container` class is invariant. A `Container[Cat]` is _not_ a `Container[Animal]`, nor is the reverse true. +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. ```scala mdoc -class Container[A](value: A) { - private var _value: A = value - def getValue: A = _value - def setValue(value: A): Unit = { - _value = value +def printAnimalNames(animals: List[Animal]): Unit = + animals.foreach { + animal => println(animal.name) } -} -``` -It may seem like a `Container[Cat]` should naturally also be a `Container[Animal]`, but allowing a mutable generic class to be covariant would not be safe. In this example, it is very important that `Container` is invariant. Supposing `Container` was actually covariant, something like this could happen: - -``` -val catContainer: Container[Cat] = new Container(Cat("Felix")) -val animalContainer: Container[Animal] = catContainer -animalContainer.setValue(Dog("Spot")) -val cat: Cat = catContainer.getValue // Oops, we'd end up with a Dog assigned to a Cat -``` +val cats: List[Cat] = List(Cat("Whiskers"), Cat("Tom")) +val dogs: List[Dog] = List(Dog("Fido"), Dog("Rex")) -Fortunately, the compiler stops us long before we could get this far. +// prints: Whiskers, Tom +printAnimalNames(cats) -### Other Examples +// prints: Fido, Rex +printAnimalNames(dogs) +``` -Another example that can help one understand variance is `trait Function1[-T, +R]` from the Scala standard library. `Function1` represents a function with one parameter, where the first type parameter `T` represents the parameter type, and the second type parameter `R` represents the return type. A `Function1` is contravariant over its parameter type, and covariant over its return type. For this example we'll use the literal notation `A => B` to represent a `Function1[A, B]`. +### Contravariance -Assume the similar `Cat`, `Dog`, `Animal` inheritance tree used earlier, plus the following: +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. ```scala mdoc -abstract class SmallAnimal extends Animal -case class Mouse(name: String) extends SmallAnimal + 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")) ``` -Suppose we're working with functions that accept types of animals, and return the types of food they eat. If we would like a `Cat => SmallAnimal` (because cats eat small animals), but are given a `Animal => Mouse` instead, our program will still work. Intuitively an `Animal => Mouse` will still accept a `Cat` as an argument, because a `Cat` is an `Animal`, and it returns a `Mouse`, which is also a `SmallAnimal`. Since we can safely and invisibly substitute the former with the latter, we can say `Animal => Mouse` is a subtype of `Cat => SmallAnimal`. +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. + +More formally, that gives us the reverse relationship: given some `class Contra[-T]`, then if `A` is a subtype of `B`, `Cov[B]` is a subtype of `Cov[A]`. ### Comparison With Other Languages Variance is supported in different ways by some languages that are similar to Scala. For example, variance annotations in Scala closely resemble those in C#, where the annotations are added when a class abstraction is defined (declaration-site variance). In Java, however, variance annotations are given by clients when a class abstraction is used (use-site variance). + +Scalas tendency towards immutable types makes it that covariant and contravariant types are more common than in other languages, since a mutabable generic type can't be covariant or contravariant. From 8cb441957d96d6de21a7e3ed38d8156df4a20883 Mon Sep 17 00:00:00 2001 From: Martijn Hoekstra Date: Fri, 24 Jun 2022 16:02:59 +0200 Subject: [PATCH 2/3] Remove joke When you're trying to understand something, jokes are never funny --- _tour/variances.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_tour/variances.md b/_tour/variances.md index 3a68b4ff60..e550ce8d14 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -56,7 +56,7 @@ Why could this be a problem? We can get the cat from the box, and it's still an mayAnimalBox.content = Dog("fido") ``` -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. Doing so would not only be unpleasant for the dog, if we try to get the cat from our catbox, it would turn out to be a dog, breaking type soundness +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 catbox, it would turn out to be a dog, breaking type soundness. ```scala val myCat: Cat = myCatBox.content //myCat would be fido the dog! From 1ad1a918095bee155a45eddce3672b9f8de42816 Mon Sep 17 00:00:00 2001 From: Martijn Hoekstra Date: Tue, 16 Aug 2022 23:28:11 +0200 Subject: [PATCH 3/3] address review comments --- _tour/variances.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/_tour/variances.md b/_tour/variances.md index e550ce8d14..beac10d015 100644 --- a/_tour/variances.md +++ b/_tour/variances.md @@ -10,7 +10,7 @@ previous-page: generic-classes redirect_from: "/tutorials/tour/variances.html" --- -Variance is a property of parameterized types that relates the subtyping relationship of parameterized types to the subtyping releationship of its type parameter. 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. +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. ```scala mdoc class Foo[+A] // A covariant class @@ -20,7 +20,7 @@ class Baz[A] // An invariant class ### Invariance -By default, type paramters in scala are invariant: subtyping relationships between the type paramter 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. +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. ```scala mdoc class Box[A](var content: A) @@ -39,13 +39,13 @@ case class Dog(name: String) extends Animal 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: ```scala mdoc - val myAnimal: Animal = Cat("felix") + val myAnimal: Animal = Cat("Felix") ``` -What about boxes? If I have a `Box[Cat]`, is it 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: +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: ```scala - val myCatBox: Box[Cat] = new Box[Cat](Cat("felix")) + val myCatBox: Box[Cat] = new Box[Cat](Cat("Felix")) val myAnimalBox: Box[Animal] = myCatBox // this doesn't compile val myAnimal: Animal = myAnimalBox.content ``` @@ -53,13 +53,13 @@ What about boxes? If I have a `Box[Cat]`, is it a subtype of `Box[Animal]`, like 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 ```scala - mayAnimalBox.content = Dog("fido") + mayAnimalBox.content = Dog("Fido") ``` -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 catbox, it would turn out to be a dog, breaking type soundness. +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. ```scala - val myCat: Cat = myCatBox.content //myCat would be fido the dog! + val myCat: Cat = myCatBox.content //myCat would be Fido the dog! ``` From this, we have to conclude that `Box[Cat]` and `Box[Animal]` can't have a subtyping relationship, even though `Cat` and `Animal` do. @@ -100,7 +100,7 @@ printAnimalNames(dogs) ### 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. +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. ```scala mdoc abstract class Serializer[-A] { @@ -122,4 +122,4 @@ More formally, that gives us the reverse relationship: given some `class Contra[ Variance is supported in different ways by some languages that are similar to Scala. For example, variance annotations in Scala closely resemble those in C#, where the annotations are added when a class abstraction is defined (declaration-site variance). In Java, however, variance annotations are given by clients when a class abstraction is used (use-site variance). -Scalas tendency towards immutable types makes it that covariant and contravariant types are more common than in other languages, since a mutabable generic type can't be covariant or contravariant. +Scala's tendency towards immutable types makes it that covariant and contravariant types are more common than in other languages, since a mutable generic type must be invariant.