As you can tell from one look at the Scaladoc for the collections classes, Scala has a powerful type system. However, unless you’re the creator of a library, you can go a long way in Scala without having to go too far down into the depths of Scala types. But once you start creating libraries for other users, you will need to learn them. This chapter provides recipes for the most common type-related problems you’ll encounter.
Scala’s type system uses a set of symbols to express different generic type concepts, including the concepts of bounds, variance, and constraints.
Note: In this blog post, only generic type parameters and variance are discussed. See the Scala Cookbook for the complete discussion.
Generic type parameters
When you first begin writing Scala code you’ll use types like
String, and custom types you create, like
Pizza, and so on. Then you’ll create traits, classes, and methods that use those types. Here’s an example of a method that uses the
Int type, as well as a
// ignore possible errors that can occur def first(xs: Seq[Int]): Int = xs(0)
Seq[Int] is a situation where one type is a container of another type.
Option[Int] are also examples of types that contain another type.
As you become more experienced working with types, when you look at the
first method you’ll see that its return type has no dependency at all on what’s inside the
Seq container. The
Seq can contain types like
Bird, and so on, and the body of the method would never change. As a result, you can rewrite that method using a generic type, like this:
def first[A](xs: Seq[A]): A = xs(0) ___ _ _
The underlined portions of the code show how a generic type is specified. Reading from right to left in the code:
As noted, the type is not referenced in the method body, there’s only
Ais used as the method return type, instead of
Ais used inside the
Seq, instead of
Ais specified in brackets, just prior to the method declaration
Specifying the generic type in brackets is a way to tell the compiler and other readers of the code that you’re about to see a generic type used in the remainder of the method signature.
Writing generic code like this makes your code more useful to more people. Instead of just working for a
Seq[Int], the method now works for a
Seq[Bird], and in general — hence the word “generic” — a
Seq of any type.
By convention, when you declare generic types in Scala, the first generic type that’s specified uses the letter
A, the second generic type is
B, and so on. For instance, if Scala didn’t include tuples and you wanted to declare your own class that can contain two different types, you’d declare it like this:
class Pair[A,B](val a: A, val b: B)
Here are a few examples of how to use that class:
Pair(1, 2) // A and B are both Int Pair(1, "1") // A is Int, B is String Pair("1", 2.2) // A is String, B is Double
In the first example,
B happen to have the same type, and in the last two examples,
B have different types.
Finally, to round out our first generic type examples, let’s create a trait that uses generic parameters, and then a class that implements that trait. First, let’s create two little classes that the example will need, along with our previous
class Cat class Dog class Pair[A,B](val a: A, val b: B)
Given that as background, this is how you create a parameterized trait with two generic type parameters:
trait Foo[A,B]: def pair(): Pair[A, B]
Notice that you declare the types you need after the trait name, then reference those types inside the trait.
Next, here’s a class that implements that trait for dogs and cats:
class Bar extends Foo[Cat, Dog]: def pair(): Pair[Cat, Dog] = Pair(Cat(), Dog())
This first line of code declares that
Bar works for
Dog types, with
Cat being a specific replacement for
Dog being a replacement for
class Bar extends Foo[Cat, Dog]:
If you want to create another class that extends
Foo and works with a
Int, you’d write it like this:
class Baz extends Foo[String, Int]: def pair(): Pair[String, Int] = Pair("1", 2)
These examples demonstrate how generic type parameters are used in different situations.
As you work more with generic types, you’ll find that you want to define certain expectations and limits on those types. To handle those situations you’ll use bounds, variance, and type constraints, which are discussed next.
Again, only variance is covered in this blog post.
As its name implies, variance is a concept that’s related to how generic type parameters can vary when subclasses of your type are created. Scala uses what’s known as declaration-site variance, which means that you — the library creator — declare variance annotations on your generic type parameters when you create new types like traits and classes. This is the opposite of Java, which uses use-site variance, meaning that clients of your library are responsible for understanding these annotations.
Because we use collections like
ArrayBuffer all the time, I find that it’s easiest to demonstrate variance when creating new types like those. So as an example, I’ll create a new type named
Container that contains one element. When I define
Container, variance has to do with whether I define its generic type parameter
class Container[A](a: A) ... // invariant class Container[+A](a: A) ... // covariant class Container[-A](a: A) ... // contravariant
How I declare
A now affects how
Container instances can be used later. For example, variance comes into play in discussions like this:
When I define a new
Containertype using one of those annotations,
If I also define a class
Dogthat’s a subtype of
Container[Dog]a subtype of
In concrete terms, what this means is that if you have a method like this that’s defined to accept a parameter of type
def foo(c: Container[Animal]) = ???
can you pass a
Two ways to simplify variance
Variance can take a few steps to explain because you have to talk about both (a) how the generic parameter is initially declared as well as (b) how instances of your container are later used, but I’ve found that there are two ways to simplify the topic.
1) If everything is immutable
The first way to simplify variance is to know that if everything in Scala was immutable, there would be little need for variance. Specifically, in a totally immutable world where all fields are
val and all collections are immutable (like
Dog is a subclass of
Container[Dog] will definitely be a subclass of
Note: As you’ll see in the following discussion, this means that the need for invariance goes away.
This is demonstrated in the following code. First I create an
Animal trait and then a
Dog case class that extends
sealed trait Animal: def name: String case class Dog(name: String) extends Animal
Now I define my
Container class, declaring its generic type parameter as
+A, making it covariant. While that’s a fancy mathematical term, it just means that when a method is declared to take a
Container[Animal], you can pass it a
Container[Dog]. Because the type is covariant, it’s flexible, and is allowed to vary in this direction, i.e., allowed to accept a subtype:
class Container[+A](a: A): def get: A = a
Then I create an instance of a
Dog as well as a
Container[Dog], and then verify that the
get method in the
Container works as desired:
val d = Dog("Fido") val h = Container[Dog](d) h.get // Dog(Fido)
To finish the example, I define a method that takes a
def printName(c: Container[Animal]) = println(c.get.name)
Finally, I pass that method a
Container[Dog] variable, and the method works as desired:
printName(h) // "Fido"
To recap, all of that code works because everything is immutable, and I define
Container with the generic parameter
Note that if I defined that parameter as just
A or as
-A, that code would not compile. (For more information on this, read on.)
2) Variance is related to the type’s “in” and “out” positions
There’s also a second way to simplify the concept of variance, which I summarize in the following three paragraphs:
(a) As you just saw, the
get method in the
Container class only uses the type
A as its return type. This is no coincidence: Whenever you declare a parameter as
+A, it can only ever be used as the return type of
Container methods. You can think of this as being an out position, and your container is said to be a producer: methods like
get produce the type
A. In addition to the
Container[+A] class just shown, other “producer” examples are the Scala
Vector[+A] classes. With these classes, once an instance of them is created, you can never add more
A values to them. Instead, they’re immutable and read-only, and you can only access their
A values with the methods that are built into them. You can think of
Vector as being producers of elements of type
A (and derivations of
(b) Conversely, if the generic type parameter you specify is only used as input parameters to methods in your container, declare the parameter to be contravariant using
-A. This declaration tells the compiler that values of type
A will only ever be passed into your container’s methods — the “in” position — and will never be returned by them. Therefore, your container is said to be a consumer. (Note that this situation is rare compared to the other two possibilities, but in the producer/consumer discussion, it’s easiest to mention it second.)
(c) Finally, if the generic parameter is used in both the method return type position and as a method parameter inside your container, define the type to be invariant by declaring it with the symbol
A. Using this type declares that your class is both a producer and a consumer of the
A type, and as a side-effect of this flexibility, the type is invariant — meaning that it cannot vary. When a method is declared to accept a
Container[Dog], it can only accept a
Container[Dog]. This type is used when defining mutable containers, such as the
ArrayBuffer[A] class, to which you can add new elements, edit elements, and access elements.
Here are examples of these three producer/consumer situations.
In the first case, when a generic type is only used as a method return type, the container is a producer, and you mark the type as covariant with
// covariant: A is only ever used in the “out” position. trait Producer[+A]: def get: A
Note that for this use case, the C# and Kotlin languages — which also use declaration-site variance — use the keyword
out when defining
A. If Scala used
out instead of
+, the code would look like this:
trait Producer[out A]: // if Scala used 'out' instead def get: A
For the second situation, if the generic type parameter is only used as the input parameter to your container methods, the container can be thought of as a consumer. Mark the generic type as contravariant using
// contravariant: A is only ever used in the “in” position. trait Consumer[-A]: def consume(a: A): Unit
In this case, C# and Kotlin use the keyword
in to indicate that
A is only used as a method input parameter (the “in” position). If Scala had that keyword, your code would look like this:
trait Consumer[in A]: // if Scala used 'in' instead def consume(a: A): Unit
Finally, when a generic type parameter is used both as method input parameters and method return parameters, it’s considered invariant — not allowed to vary — and designated as
// invariant: A is used in the “in” and “out” positions trait ProducerConsumer[A]: def consume(a: A): Unit def produce(): A
Note: Two tables that are shown in the book have been omitted from this post.
It’s actually hard to find a consistent definition of these variance terms, but this Microsoft C# page provides good definitions, which I’ll re-phrase slightly here:
- Covariance (
Lets you use a “more derived” type than what is specified. This means that you can use a subtype where a parent type is declared. In my example this means that you can pass a
Container[Animal]method parameter is declared.
- Contravariance (
Essentially the opposite of covariance, you can use a more generic (less derived) type than what is specified. For instance, you can use a
- Invariance (
This means that the type can’t vary — you can only use the type that’s specified. If a method requires a parameter of the type
Container[Dog], you can only give it a
Container[Dog]; it won’t compile if you try to give it a
Contravariance is rarely used
To keep things consistent, I’ve mentioned contravariance second in the preceding discussions, but as a practical matter, contravariant types are rarely used. For instance, the Scala
Function1 class is one of the few classes in the standard library that declares a generic parameter to be contravariant, the
T1 parameter in this case:
Because it’s not used that often, contravariance isn’t covered in this book, but there’s a good example of it in the free, online Scala 3 Book “Variance” page.
Also note from the
Function1example that a class can accept multiple generic parameters that are declared with variance.
-T1is a parameter that’s only ever consumed in the
+Ris a type that’s only ever produced in
Another type example
For the first edition of the Scala Cookbook I wrote about How to create a timer and how to create your own Try classes. That code uses types heavily, and is still relevant for Scala 3.