Beliebte Suchanfragen
|
//

The Scala Type System: Parameterized Types and Variances, Part 1

6.3.2015 | 6 minutes of reading time

The Scala language has been published in 2004 and is continuously developed by EPFL and Typesafe . These activities are funded on the one hand by the European Union and on the other hand by industrial investors . Scala has gained popularity in recent years, and is used more and more in production – also at codecentric. This blog series discusses one aspect of the Scala type system, namely co- and contravariant type parameters (i.e. these weird plus and minus signs in the header of a generic class, as e.g. in class Box[+A]).

Introduction

This article is no introduction to Scala, since there are already many of those. Some cheat sheets and short references already exist as well. Instead, I will discuss how co- and contravariant type parameters work in Scala, and why the rules that govern them make sense. Co- and contravariance generically describes how one aspect of the language varies with an inheritance hierarchy. If it varies along the inheritance hierarchy, it is called covariant. If it varies against the inheritance hierarchy, it is called contravariant (the category theoretical origin of these names is well explained in this Atlassian blog post ). An interesting aspect of this topic is that the rules that underly the co- and covariance of type parameters in Scala can be inferred from subtype polymorphism. That means that the quite unfamiliar rules of co- and contravariance that is applied to type parameters can be traced back to a widely known and accepted concept.

To get this far, I will first discuss the basics in this post by introducing co- and contravariant type parameters. How the type checking of these type parameters works and how these checks can be explained by subtype polymorphism is a topic I will cover in the next blog post. In the third part, I will then discuss how type parameters and variances can be used sensibly.

TL;DR

No time? The results are condensed at the end of this post .

Parameterized types

Let’s have a look at the following class hierarchy of boxes and fruits.

1abstract class Fruit { def name: String }
2 
3class Orange extends Fruit { def name = "Orange" }
4 
5class Apple extends Fruit { def name = "Apple" }
6 
7abstract class Box {
8 
9  def fruit: Fruit
10 
11  def contains(aFruit: Fruit) = fruit.name.equals(aFruit.name)
12}
13 
14class OrangeBox(orange: Orange) extends Box {
15  def fruit: Orange = orange
16}
17 
18class AppleBox(apple: Apple) extends Box {
19  def fruit: Apple = apple
20}

The definition of the two classes OrangeBox and AppleBox give us additional type safety, since the return type of the method fruit is additionally restricted to Orange and Apple, respectively. This class hierarchy quickly leads to the question whether the cost of maintaining the code is worth the gains on the side of type safety.

To avoid these kinds of trade-offs, Java and Scala allow to parameterize classes. That means that you can use a type parameter instead of using a real type. Those type parameters must be declared in the definition of a class, and must be bound to a real type when instantiating that class. This is very similar to using a method parameter instead of a concrete value in a method body: the parameter is just a name for a value that is passed when invoking the method. Similarly, the type parameter can be seen as a name for a type that is bound when instantiating the class. By the way: those concrete types can be passed from type parameter to type parameter, just as values can be passed from variables to variables. The syntactical means differ slightly, though.

Let’s replace the concrete return type Fruit of Box.fruit with a type parameter F, and additionally restrict it a subtype of Fruit oder Fruit itself (by adding F <: Fruit). The modified class Box is then as follows.

1class Box[F <: Fruit](aFruit: F) {
2 
3  def fruit: F = aFruit
4 
5  def contains(aFruit: Fruit) = fruit.name == aFruit.name
6}
7 
8var appleBox = new Box[Apple](new Apple)
9 
10var orangeBox = new Box[Orange](new Orange)

By parameterizing Box, we implicitly defined at least two new types: Box[Orange] and Box[Apple]. How those types relate to each other needs to be defined with variance annotations.

Variance Annotations

The two classes Box[Fruit] and Box[Apple] in the example above do not inherit from each other – that is the assumption the Scala compiler makes when there is no variance annotation. Therefore, you cannot assign an object of type Box[Apple] to a Box[Fruit]-typed variable:

1// Illegal: Box[Apple] is no subtype of Box[Fruit]. 
2var box: Box[Fruit] = new Box[Apple](new Apple)

Variance annotations to type parameter declarations are added with a + (meaning covariance) or a - (meaning contravariance). Die class header of Box can be modified to allow the above assignment:

1abstract class Box[+F <: Fruit] {

The assignment of a Box[Apple] to a variable of type Box[Fruit] is now possible, since the covariance annotation +F made Box[Apple] a subclass of Box[Fruit].

Parameterized types are invariant, if no variance annotation is given. A variance annotation creates a type hierarchy between parameterized types that is derived from the type hierarchy of the used types. The following class diagram illustrates the inheritance relations between Box[Fruit] and Box[Apple] when declaring F invariant, covariant and contravariant.

With covariance, the type hierarchy of the injected types is used, and with contravariance, their hierarchy is inverted. With invariance, the type hierarchy is completely ignored.

What relationship makes sense between the instances of the parameterized type must be decided by the developer. In this decision however, she has to consider that type parameters with variance annotations cannot be used as deliberately as invariant type parameters. How variance annotations are checked in Scala is the topic I will cover in my next blog post.

Conclusion

From a bird’s eyes view, co- and contravariant type parameters can be seen as a tool to extend the reach of the type checker in generic classes. They offer additional type safety, which also means that this concept offers new possibilities for leveraging type hierarchies without having to give up on type safety. While developers have to fall back to using comments and conventions in other programming languages, since those languages aren’t able to guarantee type safety, you can achieve quite some mileage with the Scala type system. In the next post I will discuss how co- and contravariant type parameters are checked by the type checker, and how those rules can be inferred from subtype polymorphism.

Condensed Results

Assume that class Orange extends Fruit holds. If class Box[A] is declared, then A can be prefixed with + or -.

  • A without annotation is invariant, i.e.:
    • Box[Orange] has no inheritance relationship to Box[Fruit].
  • +A is covariant, i.e.:
    • Box[Orange] is a subtype of Box[Fruit].
    • var f: Box[Fruit] = new Box[Orange]() is allowed.
  • -A is contravariant, i.e.:
    • Box[Fruit] is a subtype of Box[Orange].
    • var f: Box[Orange] = new Box[Fruit]() is allowed.
|

share post

//

More articles in this subject area

Discover exciting further topics and let the codecentric world inspire you.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.