Table of Contents
This Introduction to Scala Types article comes from the 2nd Edition of the Scala Cookbook.
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, but when you need to go deeper, I highly recommend the book, Programming in Scala, by Odersky, Spoon, and Venners. Martin Odersky is the creator of the Scala programming language, and I think of that book as “the reference” for Scala.
Scala’s type system uses a set of symbols to express different generic type concepts, including the concepts of:
- Bounds
- Variance, and
- Constraints
Before jumping into the recipes, the most common of these symbols are summarized in the following sections.
A Note About “Programming Levels” and Types:
Way back in January of 2011, Martin Odersky defined six levels of knowledge that are needed for different types of Scala programmers. He uses the levels A1-A3 for application programmers, and L1-L3 for library designers. The “types” techniques that are demonstrated in this chapter correspond to his levels L1 through L3.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
Generic Type Parameters
When you first begin writing Scala code you’ll use types like Int
, String
, and custom types you create, like Person
, Employee
, 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 Seq[Int]
:
// 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. List[String]
and 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 Int
, String
, Fish
, 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
xs(0)
(which refers to the first, or “zeroth”, element in the sequence) -
A
is used as the method return type, instead ofInt
-
A
is used inside theSeq
, instead ofInt
-
A
is 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[Fish]
, 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, A
and B
happen to have the same type, and in the last two examples, A
and 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 Pair
class:
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 Cat
and Dog
types, with Cat
being a specific replacement for A
, and Dog
being a replacement for B
:
class Bar extends Foo[Cat, Dog]:
If you want to create another class that extends Foo
and works with a String
and 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.
Bounds
Bounds let you place restrictions on type parameters. For instance, imagine that you want to write a method that returns the uppercase version of the name
field of a type:
// this code won’t compile
def upperName[A](a: A) = a.name.toUpperCase
That code is in the ballpark of what you want, but it won’t work because there’s no guarantee that the type A
has a name
field. As a solution to this problem, if you have a type like SentientBeing
, which declares a name
field:
trait SentientBeing:
def name: String
you can correctly implement the upperName
method by using a bound, as shown in this underlined code:
def upperName[A <: SentientBeing](a: A) = a.name.toUpperCase
------------------
This tells the compiler that whatever type A
is, it must be a subclass of SentientBeing
, which is guaranteed to have a name
field. So if you have classes like these that are subclasses of SentientBeing
:
case class Dog(name: String) extends SentientBeing
case class Person(name: String, age: Int) extends SentientBeing
case class Snake(name: String) extends SentientBeing
the upperName
method will work as desired with all of those:
upperName(Dog("rover")) // "ROVER"
upperName(Person("joe", 25)) // "JOE"
upperName(Snake("Noodles")) // "NOODLES"
This is the essence of working with bounds. They give you a way to define limits — bounds, or boundaries — on the possibilities of a generic type.
The following table provides descriptions of the common “Scala bounds” symbols.
Table 1. Descriptions of Scala’s bounds symbols
Bound | Description | |
---|---|---|
|
Upper bound |
|
|
Lower bound |
|
|
Lower and upper bounds used together |
The type |
TIP: Personally I can never remember the names Upper Bound and Lower Bound, but what I can remember is that when I see
A <: B
, I read to myself, “A
is less thanB
, soA
must be a subtype ofB
.”
Lower bounds are demonstrated in several methods of the collections classes. To find examples of them, search the Scaladoc of classes like List
for the >:
symbol.
Variance
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 List
and 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 A
as A
, +A
, or -A
:
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
Container
type using one of those annotations, -
If I also define a class
Dog
that’s a subtype ofAnimal
, -
Is
Container[Dog]
a subtype ofContainer[Animal]
?
In concrete terms, what this means is that if you have a method like this that’s defined to accept a parameter of type Container[Animal]
:
def foo(c: Container[Animal]) = ???
can you pass a Container[Dog]
into foo
?
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 List
), if Dog
is a subclass of Animal
, Container[Dog]
will definitely be a subclass of Container[Animal]
.
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 Animal
:
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 Container[Animal]
parameter:
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 +A
.
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 List[+A]
and 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 List
and Vector
as being producers of elements of type A
(and derivations of A
).
(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 +A
:
// 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 -A
:
// 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 A
:
// invariant: A is used in the “in” and “out” positions
trait ProducerConsumer[A]:
def consume(a: A): Unit
def produce(): A
One way to remember the variance symbols
While I generally prefer the keywords out
and in
to declare the variance of generic parameters — at least in simple, one-parameter declarations — I’ve found that I can remember the Scala symbols this way:
-
+
means that variance is allowed in the positive (subtype) direction -
-
means that variance is allowed in the negative (supertype) direction -
No additional symbol means that no variance is allowed
Because the subtype direction is far more common than the supertype direction, it’s easy to think of this as being the “positive” direction.
Descriptions and examples of Scala type variance provides a summary of this terminology, including examples of each from the Scala standard library.
Table 2. Descriptions and examples of Scala type variance
Variance | Symbol | In or Out | Producer/Consumer | Examples |
---|---|---|---|---|
Covariant |
|
Out |
Producer |
|
Contravariant |
|
In |
Consumer |
The |
Invariant |
|
Both |
Both |
|
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 (
+A
in Scala) -
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[Dog]
where aContainer[Animal]
method parameter is declared. -
Contravariance (
-A
) -
Essentially the opposite of covariance, you can use a more generic (less derived) type than what is specified. For instance, you can use a
Container[Animal]
where aContainer[Dog]
is specified. -
Invariance (
A
) -
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 aContainer[Dog]
; it won’t compile if you try to give it aContainer[Animal]
.
Testing variance with an “implicitly” trick
As its demonstrated in this Stack Overflow post, and in the book, Zionomicon, by De Goes and Fraser, you can use the implicitly
method — which is defined in the Predef
object and automatically in the scope of all your code — to test variance definitions.
For instance, using this code from my initial variance example:
sealed trait Animal:
def name: String
case class Dog(name: String) extends Animal
class Container[+A](a: A):
def get: A = a
These REPL examples show that by using implicitly
, the Scala compiler confirms that a Container[Dog]
is a subtype of a Container[Animal]
:
scala> implicitly[Dog <:< Animal] val res0: Dog <:< Animal = generalized constraint scala> implicitly[Container[Dog] <:< Container[Animal]] val res1: Container[Dog] <:< Container[Animal] = generalized constraint
You can tell that these examples work because the code compiles without error. Conversely, if you define Container
with -A
or A
, as in this example:
class Container[A](a: A):
def get: A = a
The implicitly
code will fail to compile:
scala> implicitly[Container[Dog] <:< Container[Animal]] 1 |implicitly[Container[Dog] <:< Container[Animal]] | ^ | Cannot prove that Container[Dog] <:< Container[Animal].
This turns out to be a nice trick/technique you can use to test your variance code.
Note that in this example, the expression
A <:< B
means that when working with implicit parameters,A
must be a subtype ofB
. This “type relation” symbol isn’t discussed in this book, but see Twitter’s Scala School Advanced Type page for good examples of when and where it is needed.
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:
Function1[-T1, +R]
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.
Note: Also note from the
Function1
example that a class can accept multiple generic parameters that are declared with variance.-T1
is a parameter that’s only ever consumed in theFunction1
class, and+R
is a type that’s only ever produced inFunction1
.
(Given all this background information, two solutions to common variance problems are shown in the 2nd Edition of the Scala Cookbook.)
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
Type Constraints
In addition to bounds and variance, Scala lets you specify additional type constraints. These are written with these symbols:
A =:= B // A must be equal to B
A <:< B // A must be a subtype of B
These symbols are not covered in this book. See Programming in Scala for details and examples. Twitter’s Scala School Advanced Types page also shows brief examples of their use, where they are referred to as “type relation operators.”
Several Other Type Examples: 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.