An introduction to Scala Types (from the Scala Cookbook)

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.

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 of Int

  • A is used inside the Seq, instead of Int

  • 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

A <: B

Upper bound

A must be a subtype of B.

A >: B

Lower bound

A must be a supertype of B.

A <: Upper >: Lower

Lower and upper bounds used together

The type A has both an upper and lower bound.

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 than B, so A must be a subtype of B.”

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 of Animal,

  • Is Container[Dog] a subtype of Container[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.)

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

+A

Out

Producer

List[+A], Vector[+A]

Contravariant

-A

In

Consumer

The -T1 parameter in Function1[-T1, +R]

Invariant

A

Both

Both

Array[A], ArrayBuffer[A], mutable.Set[A]

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 a Container[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 a Container[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 a Container[Dog]; it won’t compile if you try to give it a Container[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 of B. 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 the Function1 class, and +R is a type that’s only ever produced in Function1.

(Given all this background information, two solutions to common variance problems are shown in the 2nd Edition of the Scala Cookbook.)

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.