O'Reilly logo

Learning Scala by Jason Swartz

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 10. Advanced Typing

By this point in the book you should have a pretty good understanding of the Scala language. If you have read the chapters and pursued the exercises, then you are already pretty good at defining classes, writing functions, and working with collections. You know everything you need to in order to go out and start building your own Scala applications.

However, if you want to be able to read other developers’ Scala code, read and understand the Scala API, or understand how Scala works, you will want to read this chapter. In it we will cover many of the type features that make the language possible.

One interesting feature is how the apparently high-level tuples and function literals are built with regular classes. Their fancy syntax belies their humble foundation, which you can validate by creating them as class instances:

scala> val t1: (Int, Char) = (1, 'a')
t1: (Int, Char) = (1,a)

scala> val t2: (Int, Char) = Tuple2[Int, Char](1, 'a')
t2: (Int, Char) = (1,a)

scala> val f1: Int=>Int = _ + 2
f1: Int => Int = <function1>

scala> val f2: Int=>Int = new Function1[Int, Int] { def apply(x: Int) = x * 2 }
f2: Int => Int = <function1>

Another interesting type feature is implicit classes. Implicit classes provide a type-safe way to “monkey-patch” new methods and fields onto existing classes. Through automatic conversion from the original class to the new class, methods and fields in the implicit class can be invoked directly on the original class without any changes to the class’s structure:

scala> object ImplicitClasses {
     |   implicit class Hello(s: String) { def hello = s"Hello, $s" }
     |   def test = {
     |     println( "World".hello )
     |   }
     | }
defined object ImplicitClasses

scala> ImplicitClasses.test
Hello, World

Implicit parameters share a similar behavior to implicit classes, providing parameters in the local namespace that may be added to implicit-ready methods. A method that defines some of its parameters as being “implicit” can be invoked by code that has a local implicit value, but can also be invoked with an explicit parameter:

scala> object ImplicitParams {
     |   def greet(name: String)(implicit greeting: String) = s"$greeting, $name"
     |   implicit val hi = "Hello"
     |   def test = {
     |     println( greet("Developers") )
     |   }
     | }
defined object ImplicitParams

scala> ImplicitParams.test
Hello, Developers

Finally, we’ll get down to types themselves. The type parameters we have used for classes, traits, and functions are actually quite flexible. Instead of allowing any type to be used as a type parameter, you can specify that one meet an upper bound (with <:) or a lower bound (with >:):

scala> class Base { var i = 10 }; class Sub extends Base
defined class Base
defined class Sub

scala> def increment[B <: Base](b: Base) = { b.i += 1; b }
increment: [B <: Base](b: Base)Base

Type parameters can also morph into compatible types, even when bound in a new instance. When a type parameter is specified as covariant (with +), it can change into a compatible base type. The List collection is covariant, so a list of a subclass can be converted to a list of a base class:

scala> val l: List[Base] = List[Sub]()
l: List[Base] = List()

Learning these advanced type features will give you extra tools for writing better Scala code. You will also be better able to understand the official Scala library documentation, as the library makes heavy use of advanced type features. Finally, they will help you to see and understand the machinery that installs many Scala features in place.

In the next section, we’ll take a closer look at the foundation of tuples and functions as regular classes, and how you can start taking advantage of their methods.

Tuple and Function Value Classes

If you have already read the previous chapters in this book there will be no need to introduce tuples and function values to you. They were well covered in Tuples and Function Types and Values, respectively. What we haven’t yet covered about them is that behind their special syntax is a set of regular classes.

That’s right, the special sauce that makes tuples like (1, 2, true)) and function literals like (n: String) => s"Hello, $n" possible is just… sauce. The syntax shortcuts to create these instances are short and expressive, but the actual implementation is plain old classes that you could have written yourself. Don’t be disappointed by this discovery, however. The good news is it means these high-level constructs are backed by safe, type-parameterized classes.

Tuples are implemented as instances of the TupleX[Y] case class, where “X” is a number from 1 to 22 signifying its arity (the number of input parameters). The type parameter “Y” varies from a single type parameters for Tuple1 to 22 type parameters for Tuple22. Tuple1[A] has the single field _1, Tuple2[A,B] has the fields _1 and _2, and so on. When you create a tuple with the parentheses syntax (e.g., (1, 2, true), a tuple class with the same number of parameters gets instantiated with the values. In other words, the expressive syntax of tuples is simply a shortcut to a case class you could have written yourself.

The TupleX[Y] case classes each extend a ProductX trait with the same number. These traits offer operations such as productArity, returning the arity of the tuple, and productElement, a nontype-safe way to access the nth element of a tuple. They also provide companion objects that implement unapply (see Table 9-1) to enable pattern matching on tuples.

Let’s try an example of creating a tuple not with the parentheses syntax but by instantiating the Tuple2 case class:

scala> val x: (Int, Int) = Tuple2(10, 20)
x: (Int, Int) = (10,20)

scala> println("Does the arity = 2? " + (x.productArity == 2))
Does the arity = 2? true

Tuple case classes are just a data-centric implementation of an expressive syntax. Function value classes are similar but provide a logic-centric implementation.

Function values are implemented as instances of the FunctionX[Y] trait, numbered from 0 to 22 based on the arity of the function. The type parameter “Y” varies from a single type parameter for Function0 (because the return value needs a parameter) to 23 type parameters for Function22. The actual logic for the function, whether an invocation of an existing function or a new function literal, is implemented in the class’s apply() method.

In other words, when you write a function literal, the Scala compiler converts it to the body of the apply() method in a new class extending FunctionX. This forcing mechanism makes Scala’s function values compatible with the JVM, which restricts all functions to being implemented as class methods.

Let’s try out FunctionX types by writing a function literal with the regular syntax we have used thus far, and then as the body of a FunctionX.apply() method. We’ll create an anonymous class that extends the Function1[A,B] trait:

scala> val hello1 = (n: String) => s"Hello, $n"
hello1: String => String = <function1>

scala> val h1 = hello1("Function Literals")
h1: String = Hello, Function Literals

scala> val hello2 = new Function1[String,String] {
     |   def apply(n: String) = s"Hello, $n"
     | }
hello2: String => String = <function1>

scala> val h2 = hello2("Function1 Instances")
h2: String = Hello, Function1 Instances

scala> println(s"hello1 = $hello1, hello2 = $hello2")
hello1 = <function1>, hello2 = <function1>

The function values stored in hello1 and hello2 are essentially equivalent. And the Function1 class, along with all of the other FunctionX classes, overrides toString with its name in lowercase surrounded by angle brackets. Therefore, when you print out hello1 and hello2 you get the same output, <function1>. If this looks familiar to you, it’s probably because you’ve seen this in every single code sample in the book where we have stored function values. Except, of course, where we have seen <function2> emitted by values of Function2 and so on.

The Function1 trait contains two special methods not available in Function0 or any of the other FunctionX traits. You can use them to combine two or more Function1 instances into a new Function1 instance that will execute all of the functions in order when invoked. The only restriction is that the return type of the first function must match the input type of the second function, and so on.

The method andThen creates a new function value from two function values, executing the instance on the left followed by the instance on the right. The method compose works the same way but in opposite order.

Let’s try them out with regular function literals:

scala> val doubler = (i: Int) => i*2
doubler: Int => Int = <function1>

scala> val plus3 = (i: Int) => i+3
plus3: Int => Int = <function1>

scala> val prepend = (doubler compose plus3)(1)
prepend: Int = 8

scala> val append = (doubler andThen plus3)(1)
append: Int = 5

Understanding how first-class functions are implemented as FunctionX classes is an important first step to learning Scala’s type model. The language provides a concise and expressive syntax while the compiler takes care of supporting the JVM’s less-expressive runtime model, all while supporting type-safety for more stable applications.

Implicit Parameters

In Partially Applied Functions and Currying we studied partially applied functions, where a function could be invoked without its full set of parameters. The result was a function value that could be invoked with the remaining set of unspecified parameters, invoking the original function.

What if you could invoke a function without specifying all of the parameters, but the function would actually be executed? The missing, unspecified parameters would have to come from somewhere to ensure the function would operate correctly. One approach would be to define default parameters for your function, but this would require having the function know what the correct values for the missing parameters should be.

Another approach is to use implicit parameters, where the caller provides the default value in its own namespace. Functions can define an implicit parameter, often as a separate parameter group from the other nonimplicit parameters. Invokers can then denote a local value as implicit so it can be used to fill in as the implicit parameter. When the function is invoked without specifying a value for the implicit parameter, the local implicit value is then picked up and added to the function invocation.

Use the implicit keyword to mark a value, variable, or function parameter as being implicit. An implicit value or variable, if available in the current namespace, may be used to fill in for an implicit parameter in a function invocation.

Here’s one example of a function defined with an implicit parameter. The function is defined as a method in an object to keep its namespace separate from the invoker’s namespace:

scala> object Doubly {
     |   def print(num: Double)(implicit fmt: String) = {
     |     println(fmt format num)
     |   }
     | }
defined object Doubly

scala> Doubly.print(3.724)
<console>:9: error: could not find implicit value for parameter fmt: String
              Doubly.print(3.724)

scala> Doubly.print(3.724)("%.1f")
3.7

Our new print method has an implicit parameter, so we’ll either need to specify an implicit value/variable in our namespace or add the parameter explicitly. Fortunately, adding the explicit parameter works fine.

This time we’ll add an implicit local value to invoke the print method without explicitly passing the implicit parameter:

scala> case class USD(amount: Double) {
     |   implicit val printFmt = "%.2f"
     |   def print = Doubly.print(amount)
     | }
defined class USD

scala> new USD(81.924).print
81.92

Our implicit value was picked up as the second parameter group for the Doubly.print method, without the need to explicitly pass it.

Implicit parameters are heavily used in Scala’s library. They mostly provide functionality that callers can choose to override but otherwise may ignore, such as collection builders or default collection ordering.

If you use implicit parameters, keep in mind that excessive use can make your code hard to read and understand. Developers usually like to know what’s being passed to a function they are invoking. Finding out that their function invocation included implicit parameters without their knowledge may be an unwelcome surprise. You can avoid this by limiting your implicit parameters to circumstances that support a function’s implementation without changing its expected logic or data.

Implicit Classes

Another implicit feature in Scala, similar only in nature to implicit parameters, is implicit conversions with classes. An implicit class is a type of class that provides an automatic conversion from another class. By providing an automatic conversion from instances of type A to type B, an instance of type A can appear to have fields and methods as if it were an instance of type B.

What About Implicit Defs?

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. If you want to use implicit defs in your own code, see the scala.language.implicitConversions() method in the Scaladocs for instructions on how to fully enable this feature.

The Scala compiler uses implicit conversions when it finds an unknown field or method being accessed on an instance. It checks the current namespace for any implicit conversion that (1) takes the instance as an argument and (2) implements that missing field or method. If it finds a match it will add an automatic conversion to the implicit class, supporting the field or method access on the implicit type. Of course if a match isn’t found, you will get a compilation error, which is the normal course of action for invoking unknown fields or methods on your instances.

Here is an example of an implicit class that adds a “fishes” method to any integer value. The implicit class takes an integer and defines the “fishes” method it wants to add to integers:

scala> object IntUtils {
     |   implicit class Fishies(val x: Int) {                               1
     |     def fishes = "Fish" * x                                          2
     |   }
     | }
defined object IntUtils

scala> import IntUtils._                                                    3
import IntUtils._

scala> println(3.fishes)                                                    4
FishFishFish
1

Fishies, defined inside an object, implicitly converts integers to itself …

2

… so that the fishes() method will be defined for all integers.

3

Before using it, the implicit class must be added to the namespace …

4

… and then the fishes() method can be invoked on any integer.

Implicit classes make this kind of field and method grafting possible, but there are some restrictions about how you can define and use them:

  1. An implicit class must be defined within another object, class, or trait. Fortunately, implicit classes defined within objects can be easily imported to the current namespace.
  2. They must take a single nonimplicit class argument. In the preceding example, the Int parameter was sufficient to convert an Int to a Fishies class in order to access the fishes method.
  3. The implicit class’s name must not conflict with another object, class, or trait in the current namespace. Thus, a case class could not be used as an implicit class because its automatically generated companion object would break this rule.

The preceding example follows all of these rules. It is implemented inside an object (“IntUtils”), takes a single argument with the instance to be converted, and has no name conflicts with other types. Although you can implement your implicit classes in objects, classes, or traits, I find it works better to implement them in objects. Objects aren’t subclassable, so you will never automatically pick up an implicit conversion from them. Also, you can easily add an object’s implicit classes to your namespace by importing some or all of the object’s members.

To be precise, you will never automatically pick up an implicit conversion other than the ones in the scala.Predef object. The members of this object, a part of the Scala library, are automatically added to the namespace. It includes, among other type features, implicit conversions that enable some of Scala’s expressive syntax. Among them is the arrow operator (->) that you have already used (see Tuples) to generate 2-sized tuples from any two values.

Here’s a simplified version of the implicit class that makes the arrow operator possible:

implicit class ArrowAssoc[A](x: A) {
  def ->[B](y: B) = Tuple2(x, y)
}

As an example, take the expression 1 → "a", which generates a tuple with an integer and a string. What’s really occurring is an implicit conversion of an integer to an instance of ArrowAssoc followed by an invocation of the “→” method, which finally returns a new Tuple2. But because the implicit conversion was added to the namespace… implicitly… the expression is no greater than two values separated by an arrow.

Implicit classes are a great way to add useful methods to existing classes. Used carefully, they can help to make your code more expressive. Take care to avoid hurting readability, however. You wouldn’t want developers who hadn’t seen the Fishies implicit class be forced to wonder, “What the heck is a fishes method and where is it implemented?”

Types

We just devoted several sections in this chapter to type-related features such as implicit conversions and function classes. In this section we’ll move on from type-related features to focus on the core subject of types themselves.

A class is an entity that may include data and methods, and has a single, specific definition. A type is a class specification, which may match a single class that conforms to its requirements or a range of classes. The Option class, for example, is a type, but so is Option[Int]. A type could be a relation, specifying “class A or any of its descendants,” or “class B or any of its parent classes.” It could also be more abstract, specifying “any class that defines this method.”

The same can be said for traits, being entities that can include data and methods and have single, specific definitions. Types are class specifications but work equally well with traits. Objects, however, are not considered to be types. They are singletons, and while they may extend types they are not types themselves.

The examples I have used to describe the concept of types in Scala all pertain to some wonderful features we’ll explore in this section. They can help you to write stricter, safer, stabler, and better-documented code, which is the entire point of having a strong type system.

We’ll start with the ability to define your own types without creating a single class.

Type Aliases

A type alias creates a new named type for a specific, existing type (or class). This new type alias is treated by the compiler as if it were defined in a regular class. You can create a new instance from a type alias, use it in place of classes for type parameters, and specify it in value, variable, and function return types. If the class being aliased has type parameters, you can either add the type parameters to your type alias or fix them with specific types.

Like implicit conversions, however, type aliases can only be defined inside objects, classes, or traits. They only work on types, also, so objects cannot be used to create type aliases.

You can use the type keyword to define a new type alias.

Syntax: Defining a Type Alias

type <identifier>[type parameters] = <type name>[type parameters]

Okay, this is the type section, so let’s create some types!

scala> object TypeFun {
     |   type Whole = Int
     |   val x: Whole = 5
     |
     |   type UserInfo = Tuple2[Int,String]
     |   val u: UserInfo = new UserInfo(123, "George")
     |
     |   type T3[A,B,C] = Tuple3[A,B,C]
     |   val things = new T3(1, 'a', true)
     | }
defined object TypeFun

scala> val x = TypeFun.x
x: TypeFun.Whole = 5

scala> val u = TypeFun.u
u: TypeFun.UserInfo = (123,George)

scala> val things = TypeFun.things
things: (Int, Char, Boolean) = (1,a,true)

In this example, the type Whole is now an alias for the abstract class Int. Also, the type UserInfo is an alias for a tuple with an integer in the first position and a string in the second. Because a Tuple2 is an instantiable case class, we were able to instantiate it directly from the type alias UserInfo. Finally, our T3 type doesn’t fix its type parameters, and so can be instantiated with any types.

Type aliases are a useful way to refer to existing types with a local, specific name. A Tuple2[Int,String] used regularly inside a class may be more useful if it were named UserInfo. However, as with other advanced type features, type aliases should not replace careful object-oriented design. A real class named UserInfo will be more stable and intuitive in the long term than using type aliases.

Abstract Types

Whereas type aliases resolve to a single class, abstract types are specifications that may resolve to zero, one, or many classes. They work in a similar way to type aliases, but being specifications they are abstract and cannot be used to create instances. Abstract types are popularly used for type parameters to specify a range of acceptable types that may be passed. They can also be used to create type declarations in abstract classes, which declare types that concrete (nonabstract) subclasses must implement.

As an example of the latter, a trait may contain a type alias with an unspecified type. That type declaration can be reused in method signatures, and must be filled in by a subclass.

Let’s create such a trait:

scala> class User(val name: String)
defined class User

scala> trait Factory { type A; def create: A }
defined trait Factory

scala> trait UserFactory extends Factory {
     |   type A = User
     |   def create = new User("")
     | }
defined trait UserFactory

The abstract type A in Factory is used as the return type from the create method. In a concrete subclass the type is redefined with a type alias to a specific class.

Another way of writing this trait and class would be to use type parameters. Here’s an example of implementing the preceding trait and class with them:

scala> trait Factory[A] { def create: A }
defined trait Factory

scala> trait UserFactory extends Factory[User] { def create = new User("") }
defined trait UserFactory

Abstract types are an alternative to type parameters when designing generic classes. If you want a parameterizable type, then type parameters work great. Otherwise, abstract types may be more suitable. The UserFactory example class works just as well with a parameterizable type versus defining its own type alias.

In this example there were no restrictions on the type allowed for subclasses of the Factory trait. However, it is often more useful to be able to specify bounds for the type, an upper or lower bound that ensures that any type implementation meets a certain standard.

Bounded Types

A bounded type is restricted to being either a specific class or else its subtype or base type. An upper bound restricts a type to only that type or one of its subtypes. Another way of saying this is that an upper bound defines what a type must be, and through polymorphism accepts subtypes. A lower bound restricts a type to only that type or else one of the base types it extends.

You can use the upper-bound relation operator (<:) to specify an upper bound for a type.

Syntax: Upper Bounded Types

<identifier> <: <upper bound type>

Before trying out a bounded type, let’s define a few classes for testing:

scala> class BaseUser(val name: String)
defined class BaseUser

scala> class Admin(name: String, val level: String) extends BaseUser(name)
defined class Admin

scala> class Customer(name: String) extends BaseUser(name)
defined class Customer

scala> class PreferredCustomer(name: String) extends Customer(name)
defined class PreferredCustomer

Now we’ll define a function that takes a parameter with an upper bound:

scala> def check[A <: BaseUser](u: A) { if (u.name.isEmpty) println("Fail!") }
check: [A <: BaseUser](u: A)Unit

scala> check(new Customer("Fred"))

scala> check(new Admin("", "strict"))
Fail!

Our type parameter A is limited to only types that are equal to or extend the BaseUser type. This makes it possible for our parameter u to access the “name” field. Without the upper-bound restriction, accessing the “name” field on an unknown type would have led to a compilation error. The exact type of the u parameter is preserved, so a future version of this check function could safely return it with the correct type if necessary.

A less restrictive form of the upper-bound operator is available using the view-bound operator (<%). While an upper bound requires a type (and is compatible with subtypes), a view bound also supports anything that can be treated as that type. Thus view bounds are open to implicit conversion, allowing types that are not the requested type but can be converted to it. An upper bound is more restrictive, because implicit conversions are not considered as part of the type requirements.

The opposite of upper bounds are lower bounds, which specify the lowest acceptable class. Use the lower-bound relation operator (>:) to specify a lower bound for a type:

Syntax: Lower Bounded Types

<identifier> >: <lower bound type>

Let’s create a function that returns no lower than a Customer type, although the actual implmentation may be lower.

scala> def recruit[A >: Customer](u: Customer): A = u match {
     |   case p: PreferredCustomer => new PreferredCustomer(u.name)
     |   case c: Customer => new Customer(u.name)
     | }
recruit: [A >: Customer](u: Customer)A

scala> val customer = recruit(new Customer("Fred"))
customer: Customer = Customer@4746fb8c

scala> val preferred = recruit(new PreferredCustomer("George"))
preferred: Customer = PreferredCustomer@4cd8db31

Although a new PreferredCustomer instance was returned, the type of the preferred value is set by the return type, which guarantees no lower than a Customer.

Bounded types can also be used to declare abstract types. Here is an example of an abstract class declaring an abstract type and using it in a declared method. The concrete (nonabstract) subclasses then implement the type declaration as a type alias and use the type alias in the defined method. The result is that implementations of the class implement the method but assure that only a compatible type is used:

scala> abstract class Card {
     |   type UserType <: BaseUser
     |   def verify(u: UserType): Boolean
     |
     | }
defined class Card

scala> class SecurityCard extends Card {
     |   type UserType = Admin
     |   def verify(u: Admin) = true
     | }
defined class SecurityCard

scala> val v1 = new SecurityCard().verify(new Admin("George", "high"))
v1: Boolean = true

scala> class GiftCard extends Card {
     |   type UserType = Customer
     |   def verify(u: Customer) = true
     | }
defined class GiftCard

scala> val v2 = new GiftCard().verify(new Customer("Fred"))
v2: Boolean = true

As with nonbounded types, the choice of using abstract types defined inside base classes versus type parameters isn’t always clear. Many developers prefer type parameters for their more expressive syntax. However, using bounded types is often preferred over nonbounded types. They not only restrict invalid type usage in the subclasses but also work as a kind of self-documentation. They make it clear which types are expected to be used with a set of classes.

Type Variance

Whereas adding upper or lower bounds will make type parameters more restrictive, adding type variance makes type parameters less restrictive. Type variance specifies how a type parameter may adapt to meet a base type or subtype.

By default, type parameters are invariant. An instance of a type-parameterized class is only compatible with that class and parameterized type. It could not be stored in a value where the type parameter is a base type.

This behavior often surprises developers, who are familiar with Scala’s support for polymorphism. With polymorphism, a value with a given type may take the shape of one of its base types. For example, an instance of a type can be assigned to a value with the explicit type of its base type.

Here’s an example of Scala’s polymorphism, allowing lower types to be stored in values with higher types. We’ll use this two-part vehicular class hierarchy for the rest of the examples in this section:

scala> class Car { override def toString = "Car()" }
defined class Car

scala> class Volvo extends Car { override def toString = "Volvo()" }
defined class Volvo

scala> val c: Car = new Volvo()
c: Car = Volvo()

The same polymorphic adaptation doesn’t hold for type parameters, however:

scala> case class Item[A](a: A) { def get: A = a }
defined class Item

scala> val c: Item[Car] = new Item[Volvo](new Volvo)
<console>:12: error: type mismatch;
 found   : Item[Volvo]
 required: Item[Car]
Note: Volvo <: Car, but class Item is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
       val c: Item[Car] = new Item[Volvo](new Volvo)

While a Volvo instance may be assigned to a value of type Car, an Item[Volvo] instance may not be assigned to a value of type Item[Car]. Type parameters, being invariant by default, cannot adapt to alternate types even if they are compatible.

To fix this, you’ll need to make the type parameter in Item covariant. Covariant type parameters can automatically morph into one of their base types when necessary. You can mark a type parameter as being covariant by adding a plus sign (+) in front of the type parameter.

Let’s redefine the Item class with a covariant type parameter so that the Item[Volvo] type can change into Item[Car]:

scala> case class Item[+A](a: A) { def get: A = a }
defined class Item

scala> val c: Item[Car] = new Item[Volvo](new Volvo)
c: Item[Car] = Item(Volvo())

scala> val auto = c.get
auto: Car = Volvo()

The type parameter “A” is now covariant and can morph from a subtype to a base type. In other words, an instance of Item[Volvo] can be assigned to a value with the type Item[Car].

The Item.get() method likewise supports the type parameter’s covariance. While the instance is a Item[Volvo] and contains an actual Volvo, the value’s type is Item[Car] and so the return type of c.get is Car.

Covariance is a great tool for morphing type parameters into their base types. However, it is not always applicable. For example, an input parameter to a method cannot be covariant, for the same reasons that a base type cannot be converted to a subtype.

An input parameter being covariant means that it would be bound to a subtype but be invokable with a base type. This is an impossible conversion, because a base type cannot be converted to a subtype.

Let’s see what the Scala compiler says when we try to use a covariant type parameter as an input parameter type for a method:

scala> class Check[+A] { def check(a: A) = {} }
<console>:7: error: covariant type A occurs in contravariant position in
  type A of value a
       class Check[+A] { def check(a: A) = {} }

As the error from the Scala compiler explains, a type parameter used in a method parameter is contravariant, not covariant. Contravariance is where a type parameter may morph into a subtype, in the opposite direction of a polymorphic transition from subtype to base type.

Contravariant type parameters are marked with a minus sign () in front of the type parameter. They can be used for input parameters to methods but not as their return types. Return types are covariant, because their result may be a subtype that is polymorphically converted to a base type.

Let’s redefine this example with a contravariant type parameter so it can compile:

scala> class Check[-A] { def check(a: A) = {} }
defined class Check

Alternatively, you could also leave the type parameter invariant. Then the check() method could only be invoked with an input parameter of the exact same type as its class’s type parameter:

scala> class Check[A] { def check(a: A) = {} }
defined class Check

This demonstrates how to solve the “covariant parameter in contravariant position” error, but we’ll need a better example to demonstrate contravariance versus covariance. Let’s run through the experience of defining covariant and contravariant type parameters with a more comprehensive example.

In the first of two parts we’ll define the classes and methods to use. We’ll use the Car class, its subclass Volvo, and a new subclass of Volvo called VolvoWagon. With this three-level class hierarchy we can pick a middle class, Volvo, and try to replace it with either its subclass or base class. Then we’ll use Item to test covariance and Check to test contravariance. Finally we’ll define methods that require Item and Check with the middle class Volvo. This way we’ll be able to experiment with its subclass and base class to find out what works:

scala> class Car; class Volvo extends Car; class VolvoWagon extends Volvo
defined class Car
defined class Volvo
defined class VolvoWagon

scala> class Item[+A](a: A) { def get: A = a }
defined class Item

scala> class Check[-A] { def check(a: A) = {} }
defined class Check

scala> def item(v: Item[Volvo]) { val c: Car = v.get }
item: (v: Item[Volvo])Unit

scala> def check(v: Check[Volvo]) { v.check(new VolvoWagon()) }
check: (v: Check[Volvo])Unit

The Item class clearly needs a covariant type parameter. When bound to type Volvo its get() method will return a Volvo, which we should be able to store in a value with the type Car. This follows the standard rules of polymorphism where a base class value can store an instance of its subclass.

Likewise, the Check class clearly needs a contravariant type parameter. When bound to type Volvo its check() method takes a Volvo, so we should be able to pass it an instance of VolvoWagon. This also follows the standard rules of polymorphism, where an instance of a subclass can be passed to a method that expects its base class.

In the second of two parts we’ll invoke the methods with the base class, exact class, and subclass:

scala> item( new Item[Car](new Car()) )                                    1
<console>:14: error: type mismatch;
 found   : Item[Car]
 required: Item[Volvo]
              item( new Item[Car](new Car()) )
                    ^

scala> item( new Item[Volvo](new Volvo) )

scala> item( new Item[VolvoWagon](new VolvoWagon()) )                      2

scala> check( new Check[Car]() )                                           3

scala> check( new Check[Volvo]() )

scala> check( new Check[VolvoWagon]() )                                    4
<console>:14: error: type mismatch;
 found   : Check[VolvoWagon]
 required: Check[Volvo]
              check( new Check[VolvoWagon]() )
1

The Item class has a covariant type parameter, which can morph from a subclass to a base class but not the other way around.

2

Here we see covariance in action, as Item[VolvoWagon] becomes Item[Volvo].

3

Here is contravariance in action, as Check[Car] becomes Check[Volvo].

4

But not the other way around, because a contravariant type parameter cannot move from a base class to a subclass.

Covariance and contravariance can make type parameters less restrictive, but have their own restrictions about how they may be used. If you are unsure whether to use them, consider leaving your type parameters invariant. This is the default state for type parameters, and you may find it safer to keep all type parameters invariant unless a need arises to change them.

Package Objects

Most of the advanced types we have covered in this chapter, such as implicit parameters, implicit conversions, and type aliases, can only be defined within other types. This restriction helps to corral these entities, ensuring that in most cases they are only added to the namespace explicitly through imports.

One exception is the scala.Predef object, whose contents are added automatically to the namespace in Scala. Another exception is through package objects, a unique object for each package that also gets imported into the namespace of any code in that package.

Package objects are defined in their own file, package.scala, located in the package they will be affecting. You can define a package object by adding the package keyword before the object in the definition.

Here is an example package object that defines a new type alias, Mappy:

// located on com/oreilly/package.scala
package object oreilly {
  type Mappy[A,B] = collection.mutable.HashMap[A,B]
}

Any class, trait, or object defined in this package will pick up the Mappy[A,B] type alias and be able to use it directly.

The core “scala” package in the Scala library includes a package object like this one, adding many popular immutable collections to the namespace (albeit without fun names like “Mappy”).

Package objects are a good solution for defining type aliases, implicit conversions, and other advanced types. They extend the range of these features, removing the need to manually import a class, trait, or object just to pick them up.

Summary

Scala combines the paradigms of functional programming and object-oriented programming, supporting both first-class functions and class definitions. What we know now is that its first-class functions are class definitions.

The type features in Scala can make your classes and methods safer and more restrictive. By specifying bounds to acceptable type parameters, your code can declare its requirements and ensure type safety.

They can also make them less restrictive, while also providing the same amount of type-safety. Covariant and contravariant type parameters give your types flexibility in how they accept and return compatible types. And implicit classes and parameters free your code from the restrictions of fixed methods and explicit parameters, while preventing unexpected type violations.

At this point you should have no limitations on the Scala code you can understand. This would be a good time to review the Scala API in depth, becuase its frequent use of variance annotation and implicit parameters will be understandable now. You could go even further by reading through the source of the Scala library itself. I suggest starting with collections you’re well familiar with such as Option and Future.

In addition to working through the exercises in this chapter, you may want to start getting familiar with some of Scala’s excellent open source libraries. We have covered only a fraction of the SBT build system, but you’ll need to know it well to build more than a beginning application. Apache Spark is a popular way to do data analysis and other calculations with Scala. Typesafe, the company that manages the Scala code base, also provides the Play web framework and the Akka distributed computing framework. The Spray and Finagle libraries are great for building networked applications, but if all you need is a REST API, the Scalatra framework may be more suitable for you.

Finally, if you really enjoyed this section on Scala’s type system and want to explore more Haskell-like type safety features, check out the Scalaz library. Pronounced “Scala-Zed,” the library will help you write safer and more expressive code than we could have covered in this book. Learning the Scalaz library, as well as other projects by the Typelevel group, may also help you to become a better developer.

Questions

While this is an important chapter to read and comprehend, its techniques are rather advanced. You may not find some of them to be useful until you start writing your own libraries or advanced applications in Scala.

In this chapter I’ll depart from the standard exercises section followed in previous chapters. If you have completed all of the previous exercises then you should be familiar with developing concise classes and functions in the REPL, and larger applications in an IDE. Instead, let me ask you some questions about the advanced typing features and capabilities you have read about in this chapter.

You may want to find solutions to the questions by experimenting in the REPL or an IDE. It may also be useful to consider the questions a thought experiment to be completed when you have more experience using the language.

  1. How would you extend a function? What are some of the applications for a class or trait that extends Function1[A,B]? If you are writing such a class or trait, would you extend Function1[A,B] or choose to extend A => B ?
  2. How would you write a function type for a function that has two parameter lists, each with a single integer, and returns a single integer? If you wrote it as a FunctionX class, what would the exact class and type parameters contain?
  3. A popular use for implicit parameters is for a default setting that works most of the time but may be overridden in special cases. Assume you are writing a sorting function that takes lines of text, and the lines may start with a right-aligned number. If you want to sort using the numbers, which may be prefixed by spaces, how would you encode this ability in an implicit parameter? How would you allow users to override this behavior and ignore the numbers for sorting?
  4. Assume you wrote your own version of Option[A], calling it Perhaps[A], and implemented one or two methods to access its contents. What kind of implicit conversion would you need to provide in order to allow it to be treated as a collection? How would you be able to invoke flatMap and filter on your instance without implementing those methods?
  5. How would you implement your own string class named Characters that supports all of the JVM’s java.lang.String methods but can also be treated as a Scala collection? Would a combination of types and conversions do most of the work for you? I suggest perusing the source code for scala.Predef to find some hints.
  6. How would you add a “sum” method on all tuples, which returns the sum of all numeric values in a tuple? For example, (a, "hi", 2.5, 1, true).sum should return 3.5.
  7. A Function1 type takes type parameters, one for the input value and one for the output value. Which one should be covariant? Which one should be contravariant?

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required