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) { | def fishes = "Fish" * x | } | } defined object IntUtils scala> import IntUtils._ import IntUtils._ scala> println(3.fishes) FishFishFish
Implicit classes make this kind of field and method grafting possible, but there are some restrictions about how you can define and use them:
- 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.
-
They must take a single nonimplicit class argument. In the preceding example, the
Int
parameter was sufficient to convert anInt
to aFishies
class in order to access thefishes
method. - 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()) ) <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()) ) scala> check( new Check[Car]() ) scala> check( new Check[Volvo]() ) scala> check( new Check[VolvoWagon]() ) <console>:14: error: type mismatch; found : Check[VolvoWagon] required: Check[Volvo] check( new Check[VolvoWagon]() )
The
Item
class has a covariant type parameter, which can morph from a subclass to a base class but not the other way around.Here we see covariance in action, as
Item[VolvoWagon]
becomesItem[Volvo]
.Here is contravariance in action, as
Check[Car]
becomesCheck[Volvo]
.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.
-
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 extendFunction1[A,B]
or choose to extendA => B
? -
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? - 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?
-
Assume you wrote your own version of
Option[A]
, calling itPerhaps[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 invokeflatMap
andfilter
on your instance without implementing those methods? -
How would you implement your own string class named
Characters
that supports all of the JVM’sjava.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 forscala.Predef
to find some hints. -
How would you add a “sum” method on all tuples, which returns the sum of all numeric values in a tuple? For example,
(
should return 3.5.a
, "hi", 2.5, 1, true).sum -
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?
Get Learning Scala now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.