This chapter begins with an overview of type systems in general and highlights the main difference between static and dynamic typing. Then it shows the main features of the powerful Scala type system. The different types of polymorphisms are examined showing how Scala excels compared to Java thanks to ad hoc polymorphism through type classes. Type bounds is another powerful concept shown in this chapter. Roughly speaking, a bound lets you restrict the set of possible types that can be used in your data structures.
Due to the presence of both subtype and parametric polymorphism, you are forced to face, sooner or later, the concept of variance. You'll find out how to define your data structures as covariant, contravariant or invariant, and use bounds to satisfy the compiler complaints. You'll also meet other Scala type system niceties such as: self-type annotations, self-recursive types and abstract type members. Finally, you'll see how Scala let you simulate dynamic typing that allows you to deal with some situations where static typing is not a feasible solution.
Take into account that Scala's type system is a big subject that cannot be fully covered in a chapter or two. You can easily write an entire book about it, and the shapeless project (
) is a proof of how complex and powerful it can be. In this book you'll learn about Scala's type system features that will help you survive in your day-by-day coding sessions.https://github.com/milessabin/shapeless
“A type system is a tractable syntactic method for proving the absence of certain program behaviors by classifying phrases according to the kinds of values they compute.” This is the definition given by Benjamin C. Pierce in his excellent book Types and Programming Languages.
Put simply, a type is something that describes a set of values that have operations in common. For instance, the type Int
represents a set of integer numbers that support operations like addition, multiplication and so on.
A good type system gives you guarantees about the soundness (correctness) of your programs. It does not let you apply an operation to a value of the wrong type. For example, you cannot pass a String to a function that expects a List[Int]
and see it fail at runtime; a thing that can instead happen in dynamically typed languages. For example, the following code wouldn't compile:
def headOrZero(xs: List[Int]): Int = xs.headOption.getOrElse(0)
headOrZero("hello") // compile error
You get an error similar to the following:
[error] type mismatch;
[error] found : String("hello")
[error] required: List[Int
[error] headOrZero("hello")
[error] ^
[error] one error found
[error] (core/compile:compileIncremental) Compilation failed
In the type system jargon you say that that code doesn't type check. The most important thing to understand about this simple example is that, in a statically typed language, the failure happens at compile-time while in a dynamically typed one you run across this at runtime. This is something you, hopefully, always want to avoid.
In this section you'll see the advantages of both type systems. There's a war going on between people of both factions. We don't like wars, but both systems have advantages and disadvantages just like everything in life. We cannot deny, however, that from a typing point of view we're more about static typing. However, we understand that, sometimes, dynamic typing can be the right choice for the problem at hand. So, in this context, our motto is: “static typing whenever possible, dynamic typing only when really needed.”
In a statically typed language, type errors are caught by a compiler module, called type checker, prior to running the program. A type checker just makes a conservative approximation and gives error messages for anything that might cause a type error. This is because, analogously to the halting problem (
), it's not possible to build a type checker that can exactly predict which programs will surely result in type errors.https://en.wikipedia.org/wiki/Halting_problem
In a dynamically typed language, type checking is performed at runtime, immediately before the application of each operation, to make sure that the operand type is suitable for the operation. In a dynamic type system values have types but variables don't. That is, you can have a variable and assign it a string first and an int successively.
In the dynamic world, many type errors that can be caught at compile-time arise at runtime, which is not what you really want. Imagine being called at 3:00 a.m. by your manager since your application, after six months of being in production, broke because, due to a combination of user actions, it fell into that branch of the programs where you have the type error.
In a static type system that simply can't happen.
These are the main advantages of a statically typed language:
Here are the main advantages of the dynamic counterpart:
What makes Scala stand apart from Java is, without a doubt, its powerful type system. Even if you look at it from the only object-oriented perspective, Scala's type system is much better than Java's. In this regard, Scala has a unified type system in that there's a top type, Any, and a bottom type, Nothing. There are no orphans like the Java's primitive types. Figure 9.1 shows Scala's class hierarchy.
In Scala all values are instances of a class, included functions. For instance, the following two definitions of a function from Int
to Int
are equivalent. The first is just syntactic sugar for the second:
scala> val f: Int => Int = _ + 1
f: Int => Int = <function1>
scala> f(42)
res1: Int = 43
scala> val f: Function1[Int, Int] = new Function1[Int, Int] {
| override def apply(x: Int): Int = x + 1
| }
f: Function1[Int,Int] = <function1>
scala> f(42)
res2: Int = 43
As you can see, a one-parameter function is just an instance of the Function1[A, B]
class where A
is the type of the parameter and B
the return type.
The superclass of all classes is Any
and it has two direct subclasses. AnyVal
and AnyRef
represent two different class worlds: value classes and reference classes, respectively. Value classes correspond to the primitive types of Java. So Java's primitives are, in Scala, classes. You don't need boilerplate machinery to wrap primitives in wrapper classes when needed. Scala does it for you. You don't even have to worry about performance because, at the JVM level, Scala uses primitives wherever possible.
Scala also has the Unit
type which you use in place of void
. However, this is not the full story. Scala promotes functional programming where expressions are preferred to statements. To make a long story short, expressions always return a value, statements don't.
For example, curly braces always wrap expressions whose value is that of the last expression or Unit. This is important to know because it can save you from some pitfalls. For instance, the following line of code looks like a statement but it's actually an expression:
{ val a = 42 }
Its type is Unit, and the return value is the only inhabitant of the Unit type, that is ()
. Here's the proof:
scala> val b: Unit = { val a = 42 }
b: Unit = ()
Here's a common pitfall:
scala> val b = if (condition) "a"
Here, condition is some boolean value. What's the type of b
? Since the compiler cannot know whether condition will be true until runtime, it is not able to infer String as the type of b
. Indeed, its type is Any
. This is because, if the condition is false, the returned value is ()
, of type Unit
, and the least upper bound class between String
and Unit
is Any
.
All other classes define reference types. User-defined classes define reference types by default and they always, indirectly, subclass AnyRef
. Roughly speaking, AnyRef
corresponds to java.lang.Object
.
Figure 9.1 also shows implicit conversions, called views, between the value classes.
scala> val x: Int = 'a' // implicit conversion from Char to Int
x: Int = 97
The char a
is implicitly converted to Int
. Since Scala 2.10, you can also write your own value classes, which has some important consequences as you'll see in the next section.
Value classes are classes that extend AnyVal
instead of the default AnyRef
. There are two main advantages to this:
Int
, for example, and the compiler can be by your side from a type safety point of view.You can see it by implementing a simple value class such as the following:
class Meter(val value: Double) extends AnyVal {
def add(m: Meter): Meter = new Meter(value + m.value)
}
If you decompile this class using javap
, you will see the following signature for the add method:
public double add(double);
Hold on! In the original signature the add
method takes a Meter
type, where is it now? Basically, the Meter
class is not used in the final bytecode. It uses, instead, the primitive type it is wrapping; double
in this case. On the other hand, if you declare your class without extending AnyVal
, it will default to implicitly extend AnyRef
, which will make it a reference type:
class Meter(val value: Double) {
def add(m: Meter): Meter = new Meter(value + m.value)
}
Indeed, if you decompile it, this is what the add
method looks like:
public Meter add(Meter);
There you go! The class Meter
gets back in the final bytecode. So you get better type safety because you use an ad hoc type, thus restricting the set of acceptable values, at no performance cost because of the no allocation thingy.
The natural question that arises at this point is “Why don't you make every class a value class?” The answer is that you may not. There are very strict restrictions to what classes are good candidates to become value classes:
val
parameter whose type is not a value class. Since Scala 2.11 the parameter can also be private val
.equals
and hashCode
method.The most common use case for a value class is in the pimp-my-library pattern, which is when you extend an existing type through an implicit class. For example, you can think of adding the method stars
to the Int
class, which builds a string composed of N
star characters, where N
is the Int
:
// private val allowed since Scala 2.11.0
implicit class IntOps(private val x: Int) extends AnyVal {
def stars: String = "*" * x
}
val review: String = 5 stars
println(s"review: $review")
The output is:
review: *****
Although value classes are a huge win, what makes Scala stand, as a language, is that it provides three types of polymorphism, which is the subject of the next section.
Basically there are three types of polymorphism: Subtype, Parametric, and Ad hoc. Scala offers all of them. Java, on the other hand, has just subtype and parametric polymorphism. The king of statically typed functional programming languages, Haskell, has only parametric and ad hoc polymorphism. Even if this could sound more limiting than Scala you'll see that not having subtype polymorphism makes your life easier as a developer because you don't need to worry about variance (subject of the next section). Furthermore, ad hoc polymorphism is much more powerful than subtyping, as you'll see when you meet type classes.
This type of polymorphism is typical for object-oriented languages. The traditional example is an abstract superclass, Shape
, with subclasses Rectangle
, Square
and Circle
:
trait Shape {
def area: Double
}
class Rectangle(val width: Double, val height: Double) extends Shape {
override def area: Double = width * height
}
class Square(val width: Double) extends Shape {
override def area: Double = width * width
}
class Circle(val radius: Double) extends Shape {
override def area: Double = math.Pi * radius * radius
}
val shape: Shape = new Rectangle(10, 5)
println(s"Area: ${shape.area}")
The output is:
Area: 50
Basically, you have a common interface and each implementation provides its own meaning for the methods exposed by the interface—trait in Scala jargon. We won't spend too much time on subtype polymorphism since you are already used to it from other object-oriented languages, such as Java, C#, C++ and so on.
If subtype polymorphism is canonical in the object-oriented world, the parametric one is typical in functional languages. Indeed, when programmers talk about polymorphism, without adding any detail, object-oriented developers mean subtyping while functional programmers mean parametric polymorphism.
Parametric polymorphism also exists in some object-oriented languages and sometimes it is referred to as generic programming. For example, Java added parametric polymorphism through generics in version 5.
Here is an example of a method that uses parametric polymorphism:
def map[A, B](xs: List[A])(f: A => B): List[B] = xs map f
The map method takes a List[A]
and a function from A
to B
as input and returns a List[B]
. As you can see you don't mention concrete types but rather use type parameters—hence parametric polymorphism—to abstract over types. For example you can use that method to transform a List[Int]
into a List[String]
or, analogously, a List[String]
into a List[Int]
:
val stringList: List[String] = map(List(1, 2, 3))(_.toString)
val intList: List[Int] = map(List("1", "2", "3"))(_.toInt)
Roughly speaking, whenever your methods have type parameters, you're using parametric polymorphism.
Type classes come from the Haskell world and are a very powerful technique used to achieve ad hoc polymorphism.
First of all, a type class has nothing to do with the concept of class of object-oriented languages. In this regard a type class is best seen as a set of types that adhere to a given contract specified by the type class. If this is not very clear, don't worry; an example is worth more than a thousand words.
Consider the equality concept. You know that in order to compare two instances of a given class for equality, in Java, you need to override equals for that class. It turns out that writing a correct equality method is surprisingly difficult in object-oriented languages.
You may know that equals belongs to Object, the superclass of all Java classes. One of the problems is that the signature of equals is the following:
public boolean equals(Object other)
This means that you need to check, among other things, that other
is an instance of the class you're overriding equals for, proceed by doing ugly casts and so on. Furthermore, if you override equals you also need to override hashCode
, as it's brilliantly explained by Joshua Bloch in his book Effective Java.
Analyzing all the problems regarding equals is out of scope, obviously. The complexity in defining object equality using subtyping emerges in Chapter 30 of Programming in Scala: A Comprehensive Step-by-Step Guide, 2nd Edition by Odersky, Spoon, and Venners. Its title is Object Equality and it's about 25 pages long! The authors claim that, after studying a large body of Java code, the authors of a 2007 paper concluded that almost all implementations of equals methods are faulty.
So, that said, do you have a better alternative to compare two objects for equality? Yes, you guessed it: type classes. The recipe of a type class is not complicated. It consists of just three ingredients. First of all, capture the concept into a trait:
trait Equal[A] {
def eq(a1: A, a2: A): Boolean
}
The second thing you need to do is define a method that takes, implicitly, an instance of that trait. Typically you do it within the companion object:
object Equal {
def areEqual[A](a1: A, a2: A)(implicit equal: Equal[A]): Boolean =
equal.eq(a1, a2)
}
The areEqual
method says: “Give me two instances of any class A
for which exists an implicit instance of Equal[A]
in the current scope and I'll tell you if they are equal.” So, the last ingredient is the definition of the instances of Equal[A]
. For example, suppose you have the Person
class and you want a case-insensitive comparison between first and last names:
case class Person(firstName: String, lastName: String)
In order to be able to compare two instances of this class you just need to implement Equal[Person]
and make it available in the current scope. We'll do it in the Person
companion object because that's one of the places that are searched when looking for Equal[Person]
instances in scope:
object Person {
implicit object PersonEqual extends Equal[Person] {
override def eq(a1: Person, a2: Person): Boolean =
a1.firstName.equalsIgnoreCase(a2.firstName) &&
a1.lastName.equalsIgnoreCase(a2.lastName)
}
}
You have defined the Equal
type class and provided an implementation for the class Person. It sounds like a pattern and, actually, it is. In fact, in Scala, the type class concept is not first class as in Haskell. It's just a pattern that is possible thanks to implicits.
Here you can see it in use:
val p1 = Person("John", "Doe")
val p2 = Person("john", "doe")
val comparisonResult = Equal.areEqual(p1, p2)
The value of comparisonResult
is true. So, type classes let you model orthogonal concerns in a very elegant way. Indeed, the equality concern has nothing to do with the model, Person, if you think about it. Furthermore, the solution provided by the type class is more type safe than that provided by the equals
method. That is, the areEqual
method takes two instances of the same class and not Object
. You don't need ugly downcasts or other abominations.
Talking about implicit resolution, another place inspected when looking for implicit values is the companion object of the type class. So you could have defined the instance of Equal[Person]
also in the Equal
companion object. A thorough analysis of implicit resolution policy is out of scope, but can be found here
. In order to have a direct comparison with the object-oriented solution you can reimplement the shape example, seen in the subtype polymorphism section, using type classes.http://eed3si9n.com/implicit-parameter-precedence-again
First of all, the trait that capture the concept:
trait AreaComputer[T] {
def area(t: T): Double
}
This trait means: “Given a type T
you can compute its area, which is of type Double
.” You don't say anything about T
. The following case classes, instead, represent the model:
case class Rectangle(width: Double, height: Double)
case class Square(width: Double)
case class Circle(radius: Double)
At this point you need to provide the method that takes a T
and, implicitly, an implementation of the previous trait, plus the implementations for your model:
object AreaComputer {
def areaOf[T](t: T)(implicit computer: AreaComputer[T]): Double =
computer.area(t)
implicit val rectAreaComputer = new AreaComputer[Rectangle] {
override def area(rectangle: Rectangle): Double =
rectangle.width * rectangle.height
}
implicit val squareAreaComputer = new AreaComputer[Square] {
override def area(square: Square): Double =
square.width * square.width
}
implicit val circleAreaComputer = new AreaComputer[Circle] {
override def area(circle: Circle): Double =
math.Pi * circle.radius * circle.radius
}
}
Here is a usage example:
import AreaComputer._
val square = Square(10)
val area = areaOf(square)
It seems like type classes are a nicer and more powerful alternative to subtyping and, indeed, they are. Just think that, using type classes, you don't need to access the source code of a class to add a behavior. You just provide an implementation of the type class for that class and you're done.
Type classes are so important that very famous Scala libraries you may already have heard of, such as scalaz, cats, shapeless and so on couldn't even exist without them. You also saw that the concept is not that complex after all, it's just a pattern with the same three steps applied over and over.
Bounds in Scala are used for two main purposes:
The former is served by context bounds—the latter by upper and lower type bounds.
Context bounds are just syntactic sugar that lets you provide the implicit parameter list. So, a context bound describes an implicit value. It is used to declare that for some type A
, there is an implicit value of type B[A]
available in scope.
As an example consider the areEqual
method defined earlier in this chapter when you saw type classes:
def areEqual[A](a1: A, a2: A)(implicit equal: Equal[A]): Boolean = equal.eq(a1, a2)
You can, automatically, translate it to the analogous one that uses a context bound:
def areEqual[A: Equal](a1: A, a2: A): Boolean = implicitly[Equal[A]].eq(a1, a2)
You just need to:
A
to A: Equal
.So, yes, context bound is just syntactic sugar you can use in place of explicitly defining the implicits required (no pun intended). It's just a matter of taste choosing one syntax over the other.
However, there are cases where you may not use context bounds and you're forced to fall back on the explicit syntax. For instance, when your implicit type depends on more than one type you're out of luck with the context bound approach. Take for example the concept of serialization:
trait Serializer[A, B] {
def serialize(a: A): B
}
object Serializer {
def serialize[A, B](a: A)(implicit s: Serializer[A, B]): B = s.serialize(a)
// implementations of Serializer[A, B
}
The Serializer
trait encapsulates the concept of taking a type A
and serializing it into the B
type. As you can see, the serialize
method of the Serializer
companion object could not use the context bound syntax for Serializer[A, B]
since it takes two type parameters.
Before closing the section on context bounds let's show you a common trick used to allow easy access to type class instances. Basically, you just need to define an apply method on the companion object:
object Equal {
def apply[A: Equal]: Equal[A] = implicitly[Equal[A]
}
This will let you rewrite the areEqual
method as follows:
def areEqual[A: Equal](a1: A, a2: A): Boolean = Equal[A].eq(a1, a2)
Instead of using the implicitly[Equal[A]].eq(a1, a2)
you just write Equal[A].eq(a1, a2)
, which is cleaner and clearer. You can do that because, as you may already know, Equal[A]
is the same as Equal[A].apply
.
Scala also has the concept of view bounds but since they were deprecated in 2.11 we won't cover them here.
In Scala, type parameters may be constrained by a type bound. If you define your type without using a bound there will be no restriction on the possible types one can use. Whenever you want to restrict the type you need a type bound.
If you think about it, it makes sense to desire a restriction over the type. After all, we love static typing also because it lets us restrict the function domain. On the other hand, in dynamic typing a function takes zero or more parameters of any type and returns a value of any type.
Without further ado, here is an example of upper bound:
sealed trait Animal {
def name: String
}
case class Cat(name: String, livesLeft: Int) extends Animal
case class Dog(name: String, bonesHidden: Int) extends Animal
def name[A <: Animal](animal: A): String = animal.name
The name
method is where upper bound comes into play. The A <: Animal
type signature means: “Accept any type that is an Animal or any of its subclasses.” Lower bounds work in a similar fashion in that they restrict the type to its superclasses instead of its subclasses. The syntax is the following:
A >: Animal
This means that the type A
must be of type Animal
or any of its superclasses. In this particular case the superclass of Animal
is AnyRef
. While the usefulness of upper bounds is obvious, a lower bound might sound useless at first sight. However, when you meet variance in the next section, you'll see that you're forced to use lower bounds to make your code work in some scenarios.
Since Scala has both subtype and parametric polymorphism you're forced to face the concept of variance sooner or later. Consider the following scenario:
sealed trait Fruit {
def describe: String
}
class Orange extends Fruit {
override def describe: String = "Orange"
}
class Apple extends Fruit {
override def describe: String = "Apple"
}
class Delicious extends Apple {
override def describe: String = "Apple Delicious"
}
class Box[A
def describeContent(box: Box[Fruit]): String = ???
val oranges = new Box[Orange
describeContent(oranges) // does not compile
When you try to compile this code you get an error similar to the following:
type mismatch;
[error] found : Box[Orange
[error] required: Box[Fruit
…
What's the problem? Basically, even if Orange
is a subclass of Fruit
, there's no such relationship between Box[Orange]
and Box[Fruit]
. This came out of mixing subtype polymorphism—the Fruit
class hierarchy—with parametric polymorphism, Box[A]
. The previous error message is not the full story though. Indeed, the Scala compiler is kind enough to tell you what a possible fix could be. Indeed it continued with:
[error] Note:Orange <:Fruit, but class Box is invariant in type A.
[error] You may wish to define A as +A instead.
In this specific case, it actually tells you what to do. Before following its suggestion let's see, in Table 9.1, what are the available options when variance comes into play.
Table 9.1 Variance Types
Variance Type | Syntax | Meaning |
Covariant | Box[+A] | If B is a subtype of A, then Box[B] is also a subtype of Box[A] |
Contravariant | Box[-A] | If B is a subtype of A, then Box[A] is also a subtype of Box[B] |
Invariant | Box[A] | Even if B is a subtype of A, Box[A] and Box[B] are unrelated |
class Box[+A]
You just need to put the plus sign before the type, that's all. Well, not really, since nothing comes for free. Indeed, try to make Box more interesting by adding a method to it:
class Box[+A] {
def describe(a: A): String = ???
}
If you try to compile this code this time you'll get the following error:
covariant type A occurs in contravariant position in type A of value a
[error] def describe(a: A): String = ???
[error] ^
This time the compiler does not tell you what to do. The very reason behind this error has its roots in Category Theory, a branch of mathematics, which is, obviously, out of scope in a pragmatic book like this one. However you can, almost automatically, fixing this type of error by following some rules. For example, you can fix the previous compilation error by changing the code as follows:
class Box[+A] {
def describe[AA >: A](a: AA): String = ???
}
What are you doing here? Basically you are saying to the compiler: “Hey, I promise that my type parameter is not simply A
but AA
, which is an A
or one of its superclasses.” This way you satisfied the contravariant position the compiler was talking about.
Now, in order to better understand variance, consider the (simplified) signature of the Function1
class of the standard library:
trait Function1[-T1, +R] {
def apply(t : T1) : R
…
}
As you can see it's contravariant for its input type parameter and covariant for the output one. Some examples will hopefully make the reason for this clearer.
Suppose you have the following function and an instance of Apple
:
def f(apple: Apple, g: Apple => Apple): Fruit = g(apple)
val apple = new Apple
It's a simple higher-order function that applies the g function to the apple
object passed in.
Now, given that variance declaration for the Function1
type, experiment a bit to understand what type of functions you can pass as g by playing with the input and output type. Of course the implementations of the examples are deliberately simple because I want you to concentrate on the variance subject, not on understanding complex implementations.
First things first, consider a function that has exactly the signature required by g, that is Apple => Apple
:
val appleFunc: Apple => Apple = x => identity(x)
Of course, you can pass appleFunc
to f
since it matches exactly the g
signature:
f(apple, appleFunc)
No problem; it compiles as expected. Now consider the following function:
val fruitFunc: Fruit => Apple = x =>
if (x.describe == "Apple") new Apple
else new Delicious
It goes from Fruit
to Apple
. Should the function f
accept this type of function as g
? Yes, absolutely! Since Function1
is contravariant in its input type parameter this means that it can accept the Apple
type and all its superclasses and Fruit
, obviously, respects this rule.
It makes perfect sense if you think about it for a moment. What can a function, which goes from Fruit
to Apple
, do on the input parameter of type Fruit
? For sure—less specific things than on a subtype of Fruit
, such as Apple
. So it's safe passing a function with a less specific input type parameter or, said differently, with a contravariant type parameter.
Now that we justified the contravariant type let's try to do the same with the covariant one. Take a look again at the fruitFunc
function. If you look closely, you'll notice you are already exploiting the covariance of the output type parameter for functions. Indeed, fruitFunc
returns an instance of Apple
in one case and an instance of Delicious
, Apple
's subclass, in all other cases. This is possible due to the covariance of the output type.
It's plausible for a very simple reason, that is the caller of the function expects all the methods on Apple
to be available. Now, you have the guarantee that Apple
's subclasses have, at least, all Apple
's methods implemented. That's the reason why the only logical choice for the output type parameter is to be covariant.
At this point you could say: “OK, now I'm convinced that Function1
must be contravariant in its input type parameter and covariant in its output one. But what does this have to do with the trick used in the class Box
to make it compile?” It does if you look at the class Box
in a more abstract way. Indeed, its describe method takes an input type and returns an output type. Well, you can say that it is isomorphic to Function1
! As a matter of fact consider the following code:
val box = new Box[Apple
val f: Apple => String = box.describe
As you can see it transforms the Box
's describe method to a function through a process called eta-expansion. If you don't know it don't worry about its details; just consider it a means of coercing (converting) methods into functions.
At this point the reason the Scala compiler complained when you tried to use a covariant type in a contravariant position should start to make sense.
Of course you could constrain A
to be a subclass of Fruit
and delegate the Box
's describe method to Fruit
:
class Box[+A <: Fruit] {
def describe[AA >: A <: Fruit](a: AA): String = a.describe
}
As you can see, again, bounds to the rescue! In this case, for AA
, you have a multiple bound: one to satisfy the variance and the other to constrain it to a Fruit
type.
Before closing this paragraph here is the other common type of error you can get using variance. Consider the following class:
class ContraBox[-A] {
def get: A = ???
}
If you try to compile it you'll get the infamous error:
contravariant type A occurs in covariant position in type => A of method get
After having reasoned about Function1
and why the return type of it makes sense to be covariant you may already know the reason behind the previous error.
Just try to see the get method as a function () => A
, that is a zero-parameter function. Can you spot the problem now? The type returned by a function should be covariant, while A
is contravariant. Don't worry you can fix this too, this time using an upper bound:
class ContraBox[-A] {
def get[AA <: A]: AA = ???
}
There you go; this time the code will compile.
Of course, given the practical approach that a programming book must have, we tried to oversimplify some concepts but, from a pragmatic point of view, we think this is more than acceptable.
If this is the first time you've encountered the concept of variance, don't worry if something is not completely clear now. Working with it will become natural for you.
At this point you've seen enough about Scala's type system to survive while coding in Scala. Actually bounds, variance and ad hoc polymorphism, through type classes, are enough knowledge to let you write very abstract and polymorphic Scala code. In the following sections you'll see other features of the powerful Scala type system.
As stated at the beginning of this chapter, Scala's type system is a subject too complex and powerful to be covered in a single chapter of a book. However in the following sections you'll see other niceties of the type system.
Take into account that in Scala you can do the same thing in many different ways. For example, given a problem to model, you can do it using type classes. On the other hand, someone else may prefer self-recursive types—you'll see them in a bit. It depends both on the problem at hand and the taste of the programmer. That said, it's important to know the tools you have and equally important to choose the right one. However this last peculiarity cannot be taught; you just learn it through practice.
Self-type annotations allow you to serve, mainly, two purposes:
Here is an example for the first case:
trait Foo { self =>
def message: String
private trait Bar {
def message: String = this.message + " and Bar"
}
val fullMessage = {
object bar extends Bar
bar.message
}
}
object FooImpl extends Foo {
override def message: String = "Hello from Foo"
}
println(s"Message: ${FooImpl.fullMessage}")
First of all, you can see the self-type annotation right after the Foo
trait declaration. You'll see how to use the self keyword in a bit.
Indeed, the previous code compiles but there's an insidious bug that will make your stack explode because of a nasty recursion. Look at the Bar
trait. The programmer's intention here was to implement the Bar
's message method as the concatenation of the Foo
's message method and the “ and Bar” string. However, this.message
refers, recursively, to Bar
's message and not to Foo
's. You can easily fix this by using the self alias as follows:
private trait Bar {
def message: String = self.message + " and Bar"
}
The rest of the code remains unchanged.
The second and more important use of the self-type annotation is to provide dependencies among types. Consider this simple example:
trait Foo {
def message: String
}
trait Bar { self: Foo =>
def fullMessage: String = message + " and Bar"
}
Here you're saying: “Dear compiler, the Bar
trait depends on the Foo
trait. This is pretty obvious since we're using the message method belonging to Foo
within the fullMessage
method implementation. Now, if a client of this API tries to implement the Bar
trait without providing also an implementation of the Foo
trait, would you be so kind to raise a compilation error?” Since the compiler is kind it will fulfill your request, indeed the following attempt wouldn't compile:
object BarImpl extends Bar
The error message is something like:
illegal inheritance;
[error] self-type BarImpl.type does not conform to Bar's selftype Bar with Foo
That basically means: “Where is my Foo implementation?” Here's how you can fix it:
trait MyFoo extends Foo {
override def message: String = "Hello from Foo"
}
object BarImpl extends Bar with MyFoo
println(s"Message: ${BarImpl.fullMessage}")
Now the compiler is happy and if you run the code you get the following string printed to the console:
Message: Hello from Foo and Bar
So, here is a little recap. The syntax for self-type annotations can be of two types:
The former is just an alias for the this
keyword and can be useful in some situations. The latter is a constraint that won't make the code compile if the client does not satisfy it.
You can also impose multiple dependencies using the syntax:
self: Type1 with Type2 with Type3 …
This means that the client needs to provide implementations for Type1
, Type2
, Type3
and so on in order to make things work.
Self-type annotations are used as the foundation of the Cake Pattern, brilliantly described here
. To tell you the truth, we're not the biggest fans of the Cake Pattern when it comes to dependency injection. We prefer to use type classes or other techniques. Nevertheless, it can be useful in some situations so we suggest you go through that article.http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/
Self-type annotations also serve as a basis for self-recursive types, which are the subject of the next section.
One of the advantages of using a statically typed language is that you can use the type system to enforce some constraints. Scala provides self-recursive types, also known as F-bounded polymorphic types that—along with self types—let you put powerful constraints to your type definitions.
Terminology apart, here is one of the use cases where this could be useful. Consider the following example which does not use a self-recursive type:
trait Doubler[T] {
def double: T
}
case class Square(base: Double) extends Doubler[Square] {
override def double: Square = Square(base * 2)
}
So far so good; the compiler will not complain. The problem is that it won't complain even if you write something outrageous like the following code:
case class Person(firstname: String, lastname: String, age: Int)
case class Square(base: Double) extends Doubler[Person] {
override def double: Person = Person("John", "Smith", 42)
}
You want to avoid something like that by enforcing a compile-time check. Enter a self-recursive type:
trait Doubler[T <: Doubler[T]] {
def double: T
}
By using this definition of Doubler
you're saying: “Hey, if someone tries to extends Doubler
with a type that doesn't extend Doubler
in turn (hence self-recursive), do not compile it.” In this case the previous definition of Square
, which extends Doubler[Person]
, wouldn't compile.
Note that self-recursive types are not specific to Scala. Indeed Java uses them too. Take, for example, the Enum
definition:
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
…
}
E
extends Enum<E>
in Javanese means exactly E <: Enum[E]
.
F-bounded polymorphic types are of great help, but sometimes they are not enough to enforce the constraints you need. Indeed, the previous definition of Doubler
still has one problem. Consider the next code:
trait Doubler[T <: Doubler[T]] {
def double: T
}
case class Square(base: Double) extends Doubler[Square] {
override def double: Square = Square(base * 2)
}
case class Apple(kind: String) extends Doubler[Square] {
override def double: Square = Square(5)
}
Can you spot the problem? Look at the Apple
definition, which extends Doubler[Square]
instead of Doubler[Apple]
.
This code compiles because it respects the constraint put by the Doubler
definition. Indeed Square
extends Doubler
so it can be used in Apple
. Sometimes this is what you want in which case the self-recursive type will do. In cases when you don't want this to happen a self type can work this out:
trait Doubler[T <: Doubler[T]] { self: T =>
def double: T
}
Now if you try to compile the previous definition of Apple
, the compiler will complain by saying something like:
error: illegal inheritance;
self-type Apple does not conform to Doubler[Square]'s selftype Square
case class Apple(kind: String) extends Doubler[Square] {
^
In Scala, besides abstract methods and fields, you can also have abstract types within your trait or abstract classes. Here is a simple example:
trait Food
class Grass extends Food {
override def toString = "Grass"
}
class Fish extends Food {
override def toString = "Fish"
}
trait Animal {
type SuitableFood <: Food
def eat(food: SuitableFood): Unit = println(s"Eating $food…")
}
class Cow extends Animal {
type SuitableFood = Grass
}
class Cat extends Animal {
type SuitableFood = Fish
}
Look at the definition of SuitableFood
within the Animal
trait. Using that declaration you're just saying: “The type SuitableFood is abstract and it's a subclass of Food.” The classes that extend Animal
are responsible for refining the type definition. For example, Cow
defines the Grass
type as SuitableFood
. Similarly, the Cat
class refines SuitableFood
using the Fish
type. Now, the following code compiles as:
val grass = new Grass
val cow = new Cow
val fish = new Fish
val cat = new Cat
On the other hand, if you try to feed a cow with fish and/or a cat with grass it won't work:
cow.eat(fish) // won't compile
cat.eat(grass) // won't compile
At this point you can object: “Hey, I could have done the same thing using a type parameter instead of an abstract type member.” You're right. Indeed, the Animal
hierarchy could have been implemented as follows:
trait Animal[SuitableFood <: Food] {
def eat(food: SuitableFood): Unit = println(s"Eating $food…")
}
class Cow extends Animal[Grass
class Cat extends Animal[Fish]
The result is the same. At this point the question is: “When to prefer abstract type members to type parameters?” Well, many times it's just a matter of taste. However, when the number of type parameters is not just one, the abstract type approach could make your code easier to read.
Furthermore, we use the following rule of thumb, but take it with a grain of salt and evaluate case by case to choose which technique is best suited for the problem at hand.
We tend to use type parameters if we find the type does not make sense without citing its type parameter; otherwise we could opt for abstract types. For example, List, Option, Set and so on do not make much sense without citing their contained type. List of what? List[Int]
, List[String]
, and so on. On the other hand, in the previous example, Animal
makes perfect sense without citing the type. It's more elegant even in code. You notice it more if you explicitly use a type annotation. Compare these two declarations:
val animal: Animal = new Cow
val animal: Animal[Grass] = new Cow
Moreover, there are corner cases where abstract type members could greatly simplify the implementation of the API and its client code. For example, Bill Venners used it for ScalaTest's fixtures, as he explains here:
. Also, the very famous shapeless library, where the Scala type system is pushed to the limit, makes extensive use of abstract types. In this excellent post, Travis Brown explains brilliantly a corner case where using abstract type members made the difference: http://www.artima.com/weblogs/viewpost.jsp?thread=270195
.http://stackoverflow.com/questions/34544660/why-is-the-aux-technique-required-for-type-level-computations/34548518#34548518
Scala is a statically typed language, and this is a good thing, as you've seen so far. However, there are times when being able to use the peculiarities of dynamically typed languages can be a big plus for some types of problems.
In this regard, Scala provides two interesting mechanisms through which you can emulate dynamic programming for those parts of your application that need it. The techniques we're referring to go by the names of Structural Types and the Dynamic trait.
Structural types let you accomplish the so-called duck typing, typically found in dynamic languages. It can be summarized with a sentence: “If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.”
In duck typing, a programmer is only concerned with making sure that objects behave as demanded of them in a given context, rather than ensuring that they are of a specific class.
For example, you could say that a resource is closable if its class contains the close
method. Here is how you could model this requirement using structural types:
def closeResource(resource: { def close(): Unit }): Unit = {
println( “Closing resource… ”)
resource.close()
}
The duck typing incantation happens by defining the resource type as:
{ def close(): Unit }
It basically means: “Any type that has a zero-parameter close
method, which returns Unit
, is suitable to be passed to the closeResource
method.” You can see it in action in the following example:
class Foo {
def close(): Unit = println("Foo closed")
}
class Bar {
def close(): Unit = println("Bar closed")
}
val foo = new Foo
closeResource(foo)
val bar = new Bar
closeResource(bar)
The output is:
Closing resource…
Foo closed
Closing resource…
Bar closed
Even if structural types give you the power of duck typing, they have the advantage, over a dynamically typed language, of providing some form of type safety at compile time. For example, the following code wouldn't compile:
val baz = new Baz
closeResource(baz)
The error you get is something like:
[error] …: type mismatch;
[error] found: baz.type (with underlying type Baz)
[error] required: AnyRef{def close(): Unit}
[error] closeResource(baz)
[error] ^
This is pretty self-explanatory.
You can also require the existence of more than one method. In this case it's cleaner to define a type alias as in:
type Resource = {
def open(): Unit
def close(): Unit
}
def useResource(resource: Resource): Unit = {
resource.open()
// do what you need to do
resource.close()
}
Well, it seems like structural types have no downside, but we know that all that glitters is not gold. Indeed, the machinery behind structural types is the reflection and, as such, it has a non-trivial runtime cost. This is one of the reasons why you should strive to avoid structural types as much as you can. For instance, both of the previous examples can be elegantly solved using type classes and, by now, you should also know how to do it.
Structural types let you write generic code that will work, provided that a class has the given methods. The Dynamic marker trait, on the other hand, address the somewhat dual problem. It lets you pretend that an object has fields and/or methods that actually do not exist at declaration time. An example will make this clear:
import scala.language.dynamics
class Magic extends Dynamic {
def selectDynamic(field: String): Unit = println(s"You called $field")
}
val magic = new Magic
magic.foo
magic.bar
The output of the previous code is:
You called foo
You called bar
The first thing to do is import the feature, since it's disabled by default. Alternatively, you can add it to the scalacOptions
key in your SBT build file as follows:
scalacOptions += "-language:dynamics"
As you can see, even if the Magic
class does not contain the foo
and bar
fields, you can still call them because it extends Dynamic
. The selectDynamic
method is where the calls to fields are rooted. The string passed as parameter is the name of the field you called.
You can also easily chain calls:
class Magic extends Dynamic {
def selectDynamic(field: String): Magic = {
println(s"You called $field")
this
}
}
val magic = new Magic
magic.foo.bar
The output will be the same as the previous example. As you may have guessed the trick here is that, instead of Unit
, the method returns this.
Apart from fields, you can also fake methods:
class Magic extends Dynamic {
def applyDynamic(name: String)(args: Any*): Unit =
println(s"method '$name' called with arguments: ${args.mkString(", ")}")
}
val magic = new Magic
magic.someMethod("foo", 42, List(1, 2, 3))
The output is:
method 'someMethod' called with arguments: foo, 42, List(1, 2, 3)
The name parameter of the first section of applyDynamic
is the name of the invoked method. The args
parameter of its second section is a varargs of type Any
, that is any number and type of arguments.
There are other methods you can use to build full incantations using the Dynamic trait, but we won't cover them for space's sake so please refer to the Scala API for more info.
It's been a long road. The Scala type system is a very hard topic, and in this chapter you've seen its most used features. Don't worry if, at this time, something is not crystal clear. You'll digest these concepts while working with them.
Even from an OOP perspective, the Scala type system is superior to the Java one, since it has no distinction between primitives and reference types. This makes your code more coherent and with less boilerplate to go back and forth from the primitive world.
You've also seen that Scala offers the very powerful ad hoc polymorphism through type classes. After talking about bounds you've met the concept of variance that was introduced using a different and, maybe, more friendly approach.
In the end, after covering other goodies of the type system, you've seen what the language has to offer when it comes to dynamic typing. Take your deserved rest, then a deep breath. See you in the next chapter with an advanced type system concept and a demystification of the most common functional design patterns.