Scala Type System
Types in a programming language are checked at compile time and can be inferred by a compiler. Scala has a strong and statically typed language with a unified Type system. The two fundamental design considerations of a programming language are static versus dynamic typing and strong versus weak typing.
In static typing, a variable is bound to a particular type. In dynamic typing, the type is bound to the value instead of the variable. Scala and Java are statically typed languages, whereas JavaScript, Python, Groovy, and Ruby, are dynamically typed languages.
If a type is static and strongly typed, every variable must have a definite type. If a type is dynamic and strongly typed, every value must have a definite type. However, in the case of weak typing, a definite type is not defined; Scala, Java, and Ruby are principally strongly typed languages. Some languages, such as C and Perl, are weakly typed.
Scala brings the best of two worlds, in that it feels like a dynamically typed language, because of type inference, and at the same time, Scala gives you all the benefits of static typing in terms of an advanced object model and an advanced type system.
This chapter explores venues such as which type parameters should be covariant, contravariant, or invariant under subtyping, using implicits judiciously, and so forth.
Unified Type System
Scala has a unified type system, enclosed by the type Any at the top of the hierarchy and the type Nothing at the bottom of the hierarchy, as illustrated in Figure 8-1. All Scala types inherit from Any. The subtypes of Any are AnyVal (value types, such as Int and Boolean) and AnyRef (reference types, as in Java). As you can see in the Figure 8-1, the primitive types of Java are enclosed under AnyVal and, unlike Java, you can define your own AnyVal. And also unlike Java, Scala does not have Wrapper Types, such as Integer, to be distinguished from the primitive type, such as int.
Figure 8-1. Unified object model
As you can see in the Figure 8-1, Any is a supertype of both AnyRef and AnyVal.AnyRef corresponds to java.lang.Object, and is the supertype of all objects. AnyVal on the other hand represents the value such as int and other JVM primitives. Because of this hierarchy, it becomes possible to define methods that take Any, thus being compatible with both scala.Int instances as well as java.lang.String (see Listing 8-1).
Listing 8-1. Using Any
import scala.collection.mutable.ListBuffer
val list = ListBuffer[Any]()
val x= 2
list += x
class Book
list += new Book()
In Listing 8-1, Book extends AnyRef, and x is an Int that extends AnyVal.
You can limit a method to only be able to work on Value Types as seen in Listing 8-2.
Listing 8-2. Value Types
def test(int: AnyVal) = ()
test(5)
test(5.12)
test(new Object)
In Listing 8-2, test(5) takes an Int that extends AnyVal and test(5.12) takes a Double that also extends AnyVal. Test(new Object) takes an Object that extends AnyRef. Refer to Figure 8-1. Test(new Object) fails to compile.
The idea is that this method will only take Value Classes, be it Int or your own Value Type. So, we imply Java code is not as type-safe as Scala code. You’re probably thinking, “But Java is a statically typed language, doesn’t it give me all the safety that Scala does?” The answer to that is no. Take a look at the Listing 8-3 and spot the problem:
Listing 8-3. Java’s Type Unsafety
public class Bad {
public static void main(String[] argv) {
Object[] a = argv;
a[0] = new Object();
}
}
This is legal Java code, and here’s what happens when we run the code:
Java allows us to assign a String[] to Object[]. This is because a String is a subclass of Object, so if the array was read-only, the assignment would make sense. However, the array can be modified. The modification that we’ve demonstrated shows one of Java’s “type-unsafety” features. We’ll discuss why this happened and the complex topic of invariant, covariant, and contravariant types later in this chapter. Let’s start looking at how Scala makes the architect’s job easier and also makes the coder’s job easier.
Type Parameterization
Scala’s parameterized types are similar to generics in Java. If you are familiar with Java or C# you might already have some understanding of parameterized types. Scala’s parametrized types provide the same features as Java generics, but with extended functionalities.
Note Classes and traits that take type parameters are called generic; the types they generate are called parameterized type.
One straightforward syntactical difference is that Scala uses square brackets ([...]), while Java uses angle brackets (<...>). For example, a list of strings would be declared as shown in Listing 8-4.
Listing 8-4. Scala List of Strings
val list : List[String] = List("A", "B", "C")
Scala allows angle brackets to be used in the method name. So, to avoid ambiguities, Scala uses square brackets for parameterized types.
Types in Scala are used to define classes, abstract classes, traits, objects, and functions. Type parameterization lets you make these generic. As an example, sets can be defined as generic in the following manner: Set[T]. However, unlike Java which allows raw types, in Scala you are required to specify type parameters, that is to say, the Set[T], is a trait, but not a type because it takes a type parameter.
As a result, you cannot create variables of type Set as illustrated in Listing 8-5.
Listing 8-5. Scala Requires to Specify Type Parameters
def test(s: Set) {} // this will not compile
Instead, trait Set enables you to specify parameterized types, such as Set[String], Set[Int], or Set[AnyRef] as in Listing 8-6.
Listing 8-6. Specifying Parameter Types
def test(s: Set[AnyRef]) {}
For example, trait Set in Listing 8-6 defines a generic set where the specific sets are Set[Int] and Set[String], and so forth. Thus, Set is a trait, and Set[String] is a type. The Set is a generic trait.
Note In Scala, List, Set, and so on could also be referred as a type constructors, because they are used to create specific types. You could construct a type by specifying a type parameter. For example, List is the type constructor for List[String] and List[String] is a type. While Java allows raw types, Scala requires that you specify type parameters and does not allow you to use just a List in the place of a type, as it’s expecting a real type—not a type constructor.
In the light of inheritance, type parameters raise an important question regarding whether Set[String] be considered a subtype of Set[AnyRef]. That is, if S is a subtype of type T, then should Set[S] be considered a subtype of Set[T]? Next you will learn a generic type concept that defines the inheritance relation and answers the aforementioned question.
Variance
Variance defines inheritance relationships of parameterized types, which brings to light whether a Set[String], for example, is a subtype of Set[AnyRef]. A declaration like class Set[+A] means that Set is parameterized by a type A. The + is called a variance annotation.
Variance is an important and challenging concept. It defines the rules by which parameterized types can be passed as parameters. In the beginning of the chapter, we showed how passing a String[] (Java notation) to a method expecting an Object[] can cause problems. Java allows you to pass an array of something to a method expecting an array of something’s superclass. This is called covariance. On the surface, this makes a lot of sense. If you can pass a String to a method expecting an Object, why can’t you pass an Array[String] (Scala notation) to a method expecting an Array[Object]? Because Array is mutable; it can be written to in addition to being read from, so a method that takes an Array[Object] may modify the Array by inserting something that cannot be inserted into an Array[String].
Defining the type variance for type parameters allows you to control how parameterized types can be passed to methods. Variance comes in three flavors: invariant, covariant, and contravariant. Type parameters can be individually marked as covariant or contravariant and are by default invariant. Variance in Scala is defined by using + and - signs in front of type parameters.
Covariant Parameter Types
Covariant parameter types are designated with a + before the type parameter. A covariant type is useful for read-only containers. Scala’s List is defined as List[+T], which means that it’s covariant on type T. List is covariant because if you pass a List[String] to a method that expects a List[Any], then every element of the List satisfies the requirement that is an Any and we cannot change the contents of the List. Figure 8-2 gives a very clear picture of Covariance, i.e. if S extends T then Class[S] extends Class[T].
Figure 8-2. Covariance in Scala
Tip Covariance: If S extends T then Class[S] extends Class[T].
Let’s define an immutable class, Getable (see Listing 8-7). Once an instance of Getable is created, it cannot change, so we can mark its type, T, as covariant.
Listing 8-7. Immutable Class Getable
class Getable[+T](val data: T)
Let’s define a method that takes a Getable[Any] (see Listing 8-8).
Listing 8-8. Defining a Method That Takes a Getable
def get(in: Getable[Any]) {println("It's "+in.data)}
We define an instance of Getable[String] in Listing 8-9.
Listing 8-9. Instance of Getable
val gs = new Getable("String")
We can call get with gs:
Let’s try the same example but passing a Getable[java.lang.Double] into something that expects a Getable[Number] (see Listing 8-10).
Listing 8-10. Passing Double
def getNum(in: Getable[Number]) = in.data.intValue
def gd = new Getable(new java.lang.Double(33.3))
getNum(gd)
Yes, the covariance works the way we expect it to. We can make read-only classes covariant. That means that contravariance is good for write-only classes.
Contravariant Parameter Types
So, if covariance allows us to pass List[String] to a method that expects List[Any], what good is contravariance? Contravariance indicates if S extends T, then Class[T] extends Class[S] as illustrated in Figure 8-3.
Figure 8-3. Contravariance in Scala
Tip Contravariance: If S extends T then Class[T] extends Class[S].
Let’s first look at a write-only class, Putable (see Listing 8-11).
Listing 8-11. Putable Class
scala> class Putable[-T] {
def put(in: T) {println("Putting "+in)}
}
Next, let’s define a method that takes a Putable[String]:
And let’s declare an instance of Putable[AnyRef]:
And what happens if we try to call writeOnly?
Okay, so we can call a method that expects a Putable[String] with a Putable[AnyRef] because we are guaranteed to call the put method with a String, which is a subclass of AnyRef. Standing alone, this is not particularly valuable, but if we have a class that does something with input that results in output, the value of contravariance becomes obvious.
The inputs to a transformation are contravariant. Calling something that expects at leastany AnyRef with a String is legal and valid. But the return value can be covariant because we expect to get back a Number, so if we get an Integer, a subclass of Number, we’re okay. Let’s see how it works. We’ll define DS with a contravariant In type and a covariant Out type:
Let’s create an instance that will convert Any into an Int:
We define check, a method that takes a DS[String, Any]:
And we call check with t1:
Invariant Parameter Types
In Scala, Array[T] is invariant. This means that you can only pass an Array[String] to foo(a: Array[String]) and that you can only pass an Array[Object] to bar(a: Array[Object]). Figure 8-4 gives a clear picture of Invariant parameter types.
Figure 8-4. Invariance in Scala
This ensures that what is read from or written to the array is something of the correct type. So, for anything that’s mutable, the type parameter should be invariant. You do this by doing nothing with the type parameter. So, let’s define an invariant class (see Listing 8-12).
Listing 8-12. Defining an Invariant Class
class Holder[T](var data: T)
The class holds data of type T. Let’s write a method (see Listing 8-13).
Listing 8-13. add Method
def add(in: Holder[Int]) {in.data = in.data + 1}
Because the add method expects an Int to come out of Holder and puts an Int back into the Holder, the type of the Holder must be invariant. That does not mean that invariant containers lose their ability to hold subclasses of their declared type. A Holder[Number] can contain a Double, and an Array[Object] can contain String, Integer, and so on. Let’s put a Double into a Holder[Number]:
And we define a method that rounds the number:
We call the round method, and let’s see what we get out the other side:
We put in a Number and got back a Number. What’s the underlying class for the Number?
Great. Integer is a subclass of Number, so we can put an Integer or a Double into the Holder[Number]. We preserve the ability to use class hierarchies with invariant type parameters. Let’s finally see what happens when we try to pass a Holder[Double] into round.
So, invariant type parameters protect us when we have mutable data structures such as arrays.
Rules of Variance
So, we’ve successfully defined and used an invariant type. The invariant type was mutable, so it both returned and was called with a particular type. We created a covariant type that was an immutable holder of a value. Finally, we created a transformer that had contravariant input and covariant output. Wait, that sounds like a function. That’s right, Scala’s FunctionN traits have contravariant parameters and covariant results. This leads us to the simple rules of variance:
Type Bounds
When defining a parametrized type, bounds allow you to place restrictions on type parameters. Thus a bounded type is restricted to a specific type or its derived type.
Upper Type Bounds
An upper bound type is restricted to a specific type or one of its derived types Scala provides the upper bound relation operator (<:), which you can use to specify an upper bound for a type.
The type parameter A <: AnyRef means any type A that is a subtype of AnyRef. So the <: operator signifies that the type to the left of the <:operator must be a subtype of the type to the right of the <: operator. Moreover the type the left of the <: operator could be the same type of the right of the <: operator.
The type parameter A <: AnyRef means that the type to the left of the <: operator must be derived from the type to the right of the <: operator or the type to the left of the <: operator could be the same type of the right of the <: operator. In other words, the upper type bounds (and as we will explain in the next section, lower type bounds) restrict the allowed types that can be used for a type parameter when instantiating a type from a parameterized type as illustrated in the following Listing 8-15:
Listing 8-15. Defining an Upper Type Bound
def test[A <: AnyRef]
In Listing 8-15, the upper type bound says that any type used for parameter A must be a subtype of AnyRef.
The upper type bound is different from type variance in that type variance determines how actual types of the type are related, for example how the actual types List[AnyRef] and List[String] of the type List are related. Let’s explore this with an example illustrated in Listing 8-16.
Listing 8-16. Defining an Employee Class Hierarchy
class Employee (val name: String)
class Internal (name: String) extends Employee(name)
class FreeLancer(name: String) extends Employee(name)
class Customer (name: String)
Now define a function that takes a parameter with an upper bound as illustrated in Listing 8-17.
Listing 8-17. Defining a Function that Takes a Parameter with an Upper Bound
def employeeName [A <: Employee](emp: A) { println(emp.name) }
Now test the employeeName as shown:
employeeName (new Internal ("Paul"))
Now test with FreeLancer as shown:
employeeName (new FreeLancer ("John"))
Now test with the Customer class as shown in Listing 8-18.
Listing 8-18. The Customer Class Is Not a subtype of Employee
employeeName (new Customer ("Peter"))
As you can see, because of the upper bound restriction, Listing 8-18 does not compile, as the Customer class is not a subtype of Employee.
Lower Type Bounds
A lower bound type is restricted to a specific type or its supertype. The type selected must be equal to or a supertype of the lower bound restriction. Listing 8-19 defines a lower type bound.
Listing 8-19. Defining Lower Type Bound
class A {
type B >: List[Int]
def someMethod(a : B) = a
}
As you can see, we define type B inside class A to have a lower bound of List[Int]. We instantiate a variable st as a subtype of A as shown in Listing 8-20.
Listing 8-20. Instantiate the Subtype
scala> val st = new A { type B = Traversable[Int] }
We can call the some Method with a Set class. This is because Set, even if not a supertype of the List class, is a subtype of Traversable.
Implicit Class
Using types, especially when type inferencing makes them invisible, is simple and doesn’t take a lot of thought away from the task at hand. Well-defined types and type interactions will stay out of the library consumer’s way but guard against program errors.
We’ve seen a little bit of stuff so far that looks like magic. The String class seems to have grown methods:
You may be wondering how a Java class that is final could have additional methods on it. Well, Scala has a feature called implicit conversion. If you have an instance of a particular type, and you need another type, and there’s an implicit conversion in scope, Scala will call the implicit method to perform the conversion. For example, some date-related methods take Long, and some take java.util.Date. It’s useful to have conversions between the two. We create a method that calculates the number of days based on a Long containing a millisecond count:
We can calculate the number of days by passing a Long to the method:
Let’s try to pass a Date into the method:
But sometimes it’s valuable to convert between one type and another. We are used to the conversion in some contexts: Int Long, Int Double, and so on. We can define a method that will automatically be called when we need the conversion:
And this allows us to call millisToDays with a Date instance:
You may think that implicit conversions are dangerous and reduce type safety. In some cases that’s true. You should be very careful with them, and their use should be an explicit design choice. However, we see that sometimes implicit conversions (e.g., Int Long) are very valuable, for example, when we have a method that takes a parameter that must be a Long:
So having to type the following could get very old:
What is the scope of implicits? The Scala compiler considers an implicit in the current scope if:
When designing libraries, be careful about defining implicits, and make sure they are in as narrow a scope as is reasonable. When consuming libraries, make sure the implicits defined in the objects are narrow enough and are not going to cause problems such as getting stuff from every Option.
Implicit conversions are powerful tools and potentially very dangerous. We mean wicked dangerous. Back in the day, we put the implicit in Listing 8-21 into a library.
Listing 8-21. Implicit Conversion
implicit def oToT[T](in: Option[T]): T = in.get
This was convenient, very convenient. We no longer had to test Options. We just passed them around, and they were converted from an Option to their underlying type. And when we removed the implicit, we had 150 code changes to make. That was 150 latent defects. Using implicits to convert to a class that has a particular method is a good reason. There’s very little likelihood of damage.
Until Scala 2.10, implicit conversion was handled by implicit def methods that took the original instance and returned a new instance of the desired type. Implicit methods have been supplanted by implicit classes, which provide a safer and more limited scope for converting existing instances.
Scala 2.10 introduced a new feature called implicit classes. An implicit class is a class marked with the implicit keyword. This keyword makes the class’s primary constructor available for implicit conversions when the class is in scope.
To create an implicit class, simply place the implicit keyword in front of an appropriate class. Here’s an example:
Listing 8-22. Implicit Class
object Helper {
implicit class Greeting(val x: Int) {
def greet= "Hello " * x
}
}
To use this class, just import it into scope and call the greet method:
For an implicit class to work, its name must be in scope and unambiguous, like any other implicit value or conversion.
Implicit classes have the following restrictions:
object Helpers {
implicit class RichInt(x: Int) // OK!
}
implicit class RichDouble(x: Double) // BAD!
implicit class RichDate(date: java.util.Date) // OK!
implicit class Indexer[T](collecton: Seq[T], index: Int) // BAD!
implicit class Indexer[T](collecton: Seq[T])(implicit index: Index) // OK!
While it’s possible to create an implicit class with more than one non-implicit argument, such classes aren’t used during implicit lookup.
object Bar
implicit class Bar(x: Int) // BAD!
val x = 5
implicit class x(y: Int) // BAD!
implicit case class Baz(x: Int) // BAD!
Abstract Types
The abstract type allows you to define generic classes, but instead of using the conventional syntax, you name them inside the class as shown in Listing 8-23.
Listing 8-23. Abstract Type
trait Container {
type A
def value: A
}
In Listing 8-23, type A is an abstract type member. Now we can implement the value method which returns an Int.
Listing 8-24. Implementing the Method
object OnlyInt extends Container {
type A = Int
def value = 2
}
You can also apply constraints to an abstract type member so that the container can only store anything that is of a Number instance. Such constraint can be annotated on a type member right where we defined it first as illustrated in Listing 8-25.
Listing 8-25. Applying Constraint
trait AnyNumber{
type A <: Number
}
Let’s now mix in the trait AnyNumber as shown in Listing 8-26.
Listing 8-26. Mix in the Trait
trait Container{
type A <: Number
def value: A
}
object AnyNumber extends Container {
def value = 2
}
Higher-Kinded Types
Higher-kinded types use other types to construct a new type. This is similar to higher-order functions that take other functions as parameters. A higher-kinded type can have one or more other types as parameters. In Scala, you can do this using the type keyword.
We will use the :kind command. It allows you to check whether a type is higher kind. Let’s check it out on a simple type constructor, such as List[+A]:
:kind List
:kind -v List
Here we see that scalac can tell us that List, in fact, is a type constructor. Let’s investigate the syntax right above this information:
* -(+)-> *
This says “takes one type, returns another type.”
Something that takes two parameters, say Map[_, _] has the kind:
* -> * -(+)-> *
You can have kinds that are themselves parameterized by higher-kinded types. So, something could not only take a type, but take something that itself takes type parameters.
Summary
In this chapter you learned Scala’s rules about variance, Type bounds, and implicit classes and also a brief overview on Abstract types and higher-kinded types in Scala.