Chapter 7. The Scala Object System

The Predef Object

For your convenience, whenever you compile code, the Scala compiler automatically imports the definitions in the java.lang package (javac does this, too). On the .NET platform, it imports the system package. The compiler also imports the definitions in the analogous Scala package, scala. Hence, common Java or .NET types can be used without explicitly importing them or fully qualifying them with the java.lang. prefix, in the Java case. Similarly, a number of common, Scala-specific types are made available without qualification, such as String. Where there are Java and Scala type names that overlap, like List, the Scala version is imported last, so it “wins”.

The compiler also automatically imports the Predef object, which defines or imports several useful types, objects, and functions.

Tip

You can learn a lot of Scala by viewing the source for Predef. It is available by clicking the “source” link in the Predef Scaladoc page or you can download the full source code for Scala at [Scala].

Here is a partial list of the items imported or defined by Predef on the Java platform.

Table 7.1. Items Imported or Defined by Predef.

Types

Character, Class, Error, Function, Integer, Map, Pair, Runnable, Set, String, Throwable, Triple.

Exceptions

Exception, ArrayIndexOutOfBoundsException, ClassCastException, IllegalArgumentException, IndexOutOfBoundsException, NoSuchElementException, NullPointerException, NumberFormatException, RuntimeException, StringIndexOutOfBoundsException, UnsupportedOperationException

Values

Map, Set.

Objects

Pair, Triple.

Classes

Ensuring, ArrowAssoc.

Methods

Factory methods to create tuples, overloaded versions of exit, error, assert, assume, and require, implicit type conversion methods, I/O methods like readLine, println, and format, and a method currentThread, which calls java.lang.Thread.currentThread.


Predef declares the types and exceptions listed in the table using the type keyword. They are definitions that equal the corresponding scala.<Type> or java.lang.<Type> classes, so they behave like “aliases” or imports for the corresponding classes. For example, String is declared as follows.

type String = java.lang.String

In this case, the declaration has the same net effect as an import java.lang.String statement would have.

But didn’t we just say that definitions in java.lang are imported automatically, like String? The reason there is a type definition is to enable support for a uniform string type across all runtime environments. The definition is only redundant on the JVM.

The type Pair is an “alias” for Tuple2.

type Pair[+A, +B] = Tuple2[A, B]

There are two type parameters, A and B, one for each item in the pair. Recall from the section called “Abstract Types And Parameterized Types” in Chapter 2, Type Less, Do More that we explained the meaning of the ‘+’ in front of each type parameter.

Briefly, a Pair[A2,B2], for some A2 and B2, is a subclass of Pair[A1,B1], for some A1 and B1, if A2 is a subtype of A1 and B2 is a subtype of B1. In the section called “Understanding Parameterized Types” in Chapter 12, The Scala Type System, we’ll discuss ‘+’ and other type qualifiers in more detail.

2 comments

  1. Rob Dickens Posted 1 month, 20 days and 8 hours ago

    'Typle[Ay, By] itself is a derived type of Typle[Ax, Bx]' should be 'Tuple2[Ay, By] ... Tuple2[Ax, Bx]'

  2. Dean Wampler Posted 1 month, 6 days and 14 hours ago

    Thanks. Will fix

Add a comment

The Pair class also has a companion object Pair with an apply factory method, as discussed in the section called “Companion Objects” previously. Hence, we can create Pair instances as in this example:

val p = Pair(1, "one")

Pair.apply is called with the two arguments. The types A and B, shown in the definition of Pair, are inferred. A new Tuple2 instance is returned.

Map and Set appear in both the types and values lists. In the values list, they are assigned the companion objects scala.collection.immutable.Map and scala.collection.immutable.Set, respectively. Hence, Map and Set in Predef are values, not object definitions, because they refer to objects defined elsewhere, whereas Pair and Triple are defined in Predef itself. The types Map and Set are assigned the corresponding immutable classes.

The ArrowAssoc class defines two methods, -> and the unicode equivalent . The utility of these methods was demonstrated previously in the section called “Option, Some, and None: Avoiding nulls”, where we created a map of U.S. state capitals.

val stateCapitals = Map(
  "Alabama" -> "Montgomery",
  "Alaska"  -> "Juneau",
  // ...
  "Wyoming" -> "Cheyenne")
// ...

The definition of the ArrowAssoc class and the Map and Set values in Predef make the convenient Map initialization syntax possible. First, when Scala sees Map(…) it calls the apply method on the Map companion object, just as we discussed for Pair.

Map.apply expects zero or more Pairs (e.g., (a1, b2), (a2, b2), …), where each tuple holds a name and value. In the example, the tuple types are all inferred to be of type Pair[String,String]. The declaration of Map.apply is as follows.

object Map {
  ...
  def apply[A, B](elems : (A, B)*) : Map[A, B] = ...
}

Recall that there can be no type parameters on the Map companion object, because there can be only one instance. However, apply can have type parameters.

The apply method takes a variable-length argument list. Internally, x will be a subtype of Array[X]. So, for Map.apply, elems is of type Array[(A,B)] or Array[Tuple2[A,B]], if you prefer.

So, now that we know what Map.apply expects, how do we get from a -> b to (a, b)?

Predef also defines an implicit type conversion method called any2ArrowAssoc. The compiler knows that String does not define a -> method, so it looks for an implicit conversion in scope to a type that defines such a method, such as ArrowAssoc. The any2ArrowAssoc method performs that conversion. It has the following implementation.

implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = new ArrowAssoc(x)

It is applied to each item to the left of an arrow ->, e.g., the "Alabama" string. These strings are wrapped in ArrowAssoc instances, upon which the -> method is then invoked. This method has the following implementation.

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

When it is invoked, it is passed the string on the right-hand side of the ->. The method returns a tuple with the value, ("Alabama", "Montgomery"), for example. In this way, each key -> value is converted into a tuple and the resulting comma-separated list of tuples is passed to the Map.apply factory method.

The description may sound complicated at first, but the beauty of Scala is that this map initialization syntax is not an ad hoc language feature, such as a special-purpose operator -> defined in the language grammar. Instead, this syntax is defined with normal definitions of types and methods, combined with a few general-purpose parsing conventions, such as support for implicits. Furthermore, it is all type-safe. You can use the same techniques to write your own convenient “operators” for mini domain-specific languages (see Chapter 11, Domain-Specific Languages in Scala).

Implicit type conversions are discussed in more detail in the section called “Implicit Conversions” in Chapter 8, Functional Programming in Scala.

Next, recall from Chapter 1 that we were able to replace calls to Console.println(…) with println(…). This “bare” println method is defined in Predef, then imported automatically by the compiler. The definition calls the corresponding method in Console. Similarly, all the other I/O methods defined by Predef, e.g., readLine and format, call the corresponding Console methods.

Finally, the assert, assume, and require methods are each overloaded with various argument list options. They are used for runtime testing of boolean conditions. If a condition is false, an exception is thrown. The Ensuring class serves a similar purpose. You can use these features for Design by Contract programming, as discussed in the section called “Better Design with Design By Contract” in Chapter 13, Application Design.

For the full list of features defined by Predef, see the corresponding Scaladoc entry in [ScalaAPI2008].

Classes and Objects: Where Are the Statics?

Many object-oriented languages allow classes to have class-level constants, fields, and methods, called “static” members in Java, C# and C++. These constants, fields, and methods are not associated with any instances of the class.

An example of a class-level field is a shared logging instance used by all instances of a class for logging messages. An example of a class-level constant is the default logging “threshold” level.

An example of a class-level method is a “finder” method that locates all instances of the class in some repository that match some user-specified criteria. Another example is a factory method, as used in one of the factory-related design patterns [GOF1995].

In order to remain consistent with the goal that “everything is an object” in Scala, class-level fields and methods are not supported. Instead, Scala supports declarations of classes that are singletons, using the object keyword instead of the class keyword. The objects provide an object-oriented approach to “static” data and methods. Hence, Scala does not even have a static keyword.

Objects are instantiated automatically and lazily by the runtime system (see section 5.4 of [ScalaSpec2009]). Just as for classes and traits, the body of the object is the constructor, but since the system instantiates the object, there is no way for the user to specify a parameter list for the constructor, so they aren’t supported. Any data defined in the object has to be initialized with default values. For the same reasons, auxiliary constructors can’t be used and are not supported.

We’ve already seen some examples of objects, such as the “specs” objects used previously for tests, and the Pair type and its companion object, which we explored in the section called “The Predef Object” earlier in this chapter.

type Pair[+A, +B] = Tuple2[A, B]
object Pair {
  def apply[A, B](x: A, y: B) = Tuple2(x, y)
  def unapply[A, B](x: Tuple2[A, B]): Option[Tuple2[A, B]] = Some(x)
}

To reference an object field or method, you use the syntax object_name.field or object_name.method(…), respectively. For example, Pair.apply(…). Note that this is the same syntax that is commonly used in languages with static fields and methods.

Tip

When an object named MyObject is compiled to a class file, the class file name will be MyObject$.class.

In Java and C#, the convention for defining constants is to use final static fields. (C# also has a constant keyword for simple fields, like ints and strings.) In Scala, the convention is to use val fields in objects.

Finally, recall from the section called “Nested Classes” that class definitions can be nested within other class definitions. This property generalizes for objects. You can define nested objects, traits, and classes inside other objects, traits, and classes.

Package Objects

Scala version 2.8 introduces a new scoping construct called package objects. They are used to define types, variables, and methods that are visible at the level of the corresponding package. To understand their usefulness, let’s see an example from Scala version 2.8 itself. The collection library is being reorganized to refine the package structure and to use it more consistently (among other changes). The Scala team faced a dilemma. They wanted to move types to new packages, but avoid breaking backwards compatibility. The package object construct provided a solution, along with other benefits.

For example, the immutable List is defined in the scala package in version 2.7, but it is moved to the scala.collection.immutable package in version 2.8. Despite the change, List is made visible in the scala package using package object scala, found in the src/library/scala/package.scala file in the version 2.8 source code distribution. Note the file name. It’s not required, but it’s a useful convention for package objects. Here is the full package object definition (at the time of this writing; it could change before the 2.8.0 final version).

package object scala {
  type Iterable[+A] = scala.collection.Iterable[A]
  val Iterable = scala.collection.Iterable

  @deprecated("use Iterable instead") type Collection[+A] = Iterable[A]
  @deprecated("use Iterable instead") val Collection = Iterable

  type Seq[+A] = scala.collection.Sequence[A]
  val Seq = scala.collection.Sequence

  type RandomAccessSeq[+A] = scala.collection.Vector[A]
  val RandomAccessSeq = scala.collection.Vector

  type Iterator[+A] = scala.collection.Iterator[A]
  val Iterator = scala.collection.Iterator

  type BufferedIterator[+A] = scala.collection.BufferedIterator[A]

  type List[+A] = scala.collection.immutable.List[A]
  val List = scala.collection.immutable.List

  val Nil = scala.collection.immutable.Nil

  type ::[A] = scala.collection.immutable.::[A]
  val :: = scala.collection.immutable.::

  type Stream[+A] = scala.collection.immutable.Stream[A]
  val Stream = scala.collection.immutable.Stream

  type StringBuilder = scala.collection.mutable.StringBuilder
  val StringBuilder = scala.collection.mutable.StringBuilder
}

Note that pairs of declarations like type List[+] = … and val List = … are effectively “aliases” for the companion class and object, respectively. Because the contents of the scala package is automatically imported by the compiler, you can still reference all the definitions in this object in any scope without an explicit import statement for fully-qualified names.

Other than the way the members in package objects are scoped, they behave just like other object declarations. While this example contains only vals and types, you can also define methods and you can subclass another class or trait and mix in other traits.

Another benefit of package objects is that it provides a more succinct implementation of what was an awkward idiom before. Without package objects, you would have to put definitions in an ad hoc object inside the desired package, then import from the object. For example, here is how List would have to be handled without a package object.

1 comment

  1. ctran Posted 29 days and 23 hours ago

    Can you explain how this awkward idiom is needed?

Add a comment

package scala {
  object toplevel {
    ...
    type List[+A] = scala.collection.immutable.List[A]
    val List = scala.collection.immutable.List
    ...
  }
}

...
import scala.toplevel._
...

Finally, another benefit of package objects is the way they provide a clear separation between the abstractions exposed by a package and the implementations that should be hidden inside it. In a larger application, a package object could be used to expose all the public types, values, and operations (methods) for a “component”, while everything else in the package and nested packages could be treated as internal implementation details.

Sealed Class Hierarchies

Recall from the section called “Case Classes” in Chapter 6, Advanced Object-Oriented Programming In Scala that we demonstrated pattern matching with our Shapes hierarchy, which use case classes. We had a default case _ => … expression. It’s usually wise to have one. Otherwise, if someone defines a new subtype of Shape and passes it to this match statement, a runtime scala.MatchError will be thrown, because the new shape won’t match the shapes covered in the match statement. However, it’s not always possible to define reasonable behavior for the default case.

There is an alternative solution if you know that the case class hierarchy is unlikely to change and you can define the whole hierarchy in one file. In this situation, you can add the sealed keyword to the declaration of the common base class. When sealed, the compiler knows all the possible classes that could appear in the match expression, because all of them must be defined in the same source file. So, if you cover all those classes in the case expressions (either explicitly or through shared parent classes), then you can safely eliminate the default case expression.

Here is an example using the HTTP 1.1 methods [HTTP1.1], which are not likely to change very often, so we declare a “sealed” set of case classes for them.

// code-examples/ObjectSystem/sealed/http-script.scala

sealed abstract class HttpMethod()
case class Connect(body: String) extends HttpMethod
case class Delete (body: String) extends HttpMethod
case class Get    (body: String) extends HttpMethod
case class Head   (body: String) extends HttpMethod
case class Options(body: String) extends HttpMethod
case class Post   (body: String) extends HttpMethod
case class Put    (body: String) extends HttpMethod
case class Trace  (body: String) extends HttpMethod

def handle (method: HttpMethod) = method match {
  case Connect (body) => println("connect: " + body)
  case Delete  (body) => println("delete: "  + body)
  case Get     (body) => println("get: "     + body)
  case Head    (body) => println("head: "    + body)
  case Options (body) => println("options: " + body)
  case Post    (body) => println("post: "    + body)
  case Put     (body) => println("put: "     + body)
  case Trace   (body) => println("trace: "   + body)
}

val methods = List(
  Connect("connect body..."),
  Delete ("delete body..."),
  Get    ("get body..."),
  Head   ("head body..."),
  Options("options body..."),
  Post   ("post body..."),
  Put    ("put body..."),
  Trace  ("trace body..."))

methods.foreach { method => handle(method) }

This script outputs the following.

2 comments

  1. Rob Dickens Posted 1 month, 19 days and 20 hours ago

    only need 'case Connect(body) =>' in def handle, in Ex 7.7, 7.8

  2. Dean Wampler Posted 1 month, 6 days and 14 hours ago

    Doh! Copy & paste ...

Add a comment

connect: connect body...
delete: delete body...
get: get body...
head: head body...
options: options body...
post: post body...
put: put body...
trace: trace body...

No default case is necessary, since we cover all the possibilities. Conversely, if you omit one of the classes and you don’t provide a default case or a case for a shared parent class, the compiler warns you that the “match is not exhaustive”. For example, if you comment out the case for Put, you get this warning.

warning: match is not exhaustive!
missing combination            Put

def handle (method: HttpMethod) = method match {
...

You also get a MatchError exception if a Put instance is passed to the match.

Using sealed has one drawback. Every time you add or remove a class from the hierarchy, you have to modify the file, since the entire hierarchy has to be declared in the same file. This breaks the Open-Closed Principle ([Meyer1997] and [Martin2003]), which is a solution to the practical problem that it can be costly to modify existing code, retest it (and other code that uses it), and redeploy it. It’s much less “costly” if you can extend the system by adding new derived types in separate source files. This is why we picked the HTTP method hierarchy for the example. The list of methods is very stable.

Tip

Avoid sealed case class hierarchies if the hierarchy changes frequently (for an appropriate definition of “frequently”).

Finally, you may have noticed some duplication in the example. All the concrete classes have a body field. Why didn’t we put that field in the parent HttpMethod class? Because we decided to use case classes for the concrete classes, we’ll run into the same problem with case-class inheritance that we discussed previously in the section called “Case Class Inheritance” in Chapter 6, Advanced Object-Oriented Programming In Scala, where we added a shared id field in the Shape hierarchy. We need the body argument for each HTTP method’s constructor, yet it will be made a field of each method type automatically. So, we would have to use the override val technique we demonstrated previously.

We could remove the case keywords and implement the methods and companion objects that we need. However, in this case, the duplication is minimal and tolerable.

What if we want to use case classes, yet also reference the body field in HttpMethod? Fortunately, we know that Scala will generate a body reader method in every concrete subclass (as long as we use the name body consistently!). So, we can declare that method abstract in HttpMethod, then use it as we see fit. The following example demonstrates this technique.

// code-examples/ObjectSystem/sealed/http-body-script.scala

sealed abstract class HttpMethod() {
    def body: String
    def bodyLength = body.length
}

case class Connect(body: String) extends HttpMethod
case class Delete (body: String) extends HttpMethod
case class Get    (body: String) extends HttpMethod
case class Head   (body: String) extends HttpMethod
case class Options(body: String) extends HttpMethod
case class Post   (body: String) extends HttpMethod
case class Put    (body: String) extends HttpMethod
case class Trace  (body: String) extends HttpMethod

def handle (method: HttpMethod) = method match {
  case Connect (body) => println("connect: " + body)
  case Delete  (body) => println("delete: "  + body)
  case Get     (body) => println("get: "     + body)
  case Head    (body) => println("head: "    + body)
  case Options (body) => println("options: " + body)
  case Post    (body) => println("post: "    + body)
  case Put     (body) => println("put: "     + body)
  case Trace   (body) => println("trace: "   + body)
}

val methods = List(
  Connect("connect body..."),
  Delete ("delete body..."),
  Get    ("get body..."),
  Head   ("head body..."),
  Options("options body..."),
  Post   ("post body..."),
  Put    ("put body..."),
  Trace  ("trace body..."))

methods.foreach { method =>
  handle(method)
  println("body length? " + method.bodyLength)
}

We declared body abstract in HttpMethod. We added a simple bodyLength method that calls body. The loop at the end of the script calls bodyLength. Running this script produces the following output.

connect: connect body...
body length? 15
delete: delete body...
body length? 14
get: get body...
body length? 11
head: head body...
body length? 12
options: options body...
body length? 15
post: post body...
body length? 12
put: put body...
body length? 11
trace: trace body...
body length? 13

As always, every feature has pluses and minuses. Case classes and sealed class hierarchies have very useful properties, but they aren’t suitable for all situations.

The Scala Type Hierarchy

We have mentioned a number of types in Scala’s type hierarchy already. Let’s look at the general structure of the hierarchy, as illustrated in Figure 7.1, “Scala’s type hierarchy.”.

Figure 7.1. Scala’s type hierarchy.

images/TypeHierarchy.png

The following tables discuss the types shown in Figure 7.1, “Scala’s type hierarchy.”, as well as some other important types that aren’t shown. Some details are omitted for clarity. When the underlying “runtime” is discussed, the points made apply equally to the JVM and the .NET CLR, except where noted.

Table 7.2. Any, AnyVal, and AnyRef.

Name Parent Description

Any

none

the root of the hierarchy. Defines a few final methods like ==, !=, isInstanceOf[T] (for type checking), and asInstanceOf[T] (for type casting), as well as default versions of equals, hashCode, and toString, which are designed to be overridden by subclasses.

AnyVal

Any

The parent of all value types, which correspond to the primitive types on the runtime platform, plus Unit. All the AnyVal instances are immutable value instances and all the AnyVal types are abstract final. Hence, none of them can be instantiated with new. Rather, new instances are created with literal values (e.g., 3.14 for a Double) or by calling methods on instances that return new values.

AnyRef

Any

The parent of all reference types, including all java.* and scala.* types. It is equivalent to java.lang.Object for the JVM and object (System.Object) for the .NET runtime. Instances of reference types are created with new.


The value types are children of AnyVal.

Table 7.3. Direct subtypes of AnyVal, the value types.

Name Runtime Primitive Type

Boolean

boolean (true and false).

Byte

byte.

Char

char.

Short

short.

Int

int.

Long

long.

Float

float.

Double

double.

Unit

Serves the same role as void in most imperative languages. Used primarily as a function return value. There is only one instance of Unit, named (). Think of it as a tuple with zero items.


All other types, the reference types, are children of AnyRef. Here are some of the more commonly-used reference types. Note that there are some significant differences between the version 2.7.X and 2.8 collections.

Table 7.4. Direct and indirect subtypes of AnyRef, the reference types.

Name Parent Description

Collection[+T]

Iterable[T]

Trait for collections of known size.

Either[+T1, +T2]

AnyRef

Used most often as a return type when a method could return an instance of one of two unrelated types. For example, an exception or a “successful” result. The Either can be pattern matched for its Left or Right subtypes. (It is analogous to Option, with Some and None) For the exception-handling idiom, it is conventional to use Left for the exception.

FunctionN[-T1, -T2, …, -TN, +R]

AnyRef

Trait representing a function that takes N arguments, each of which can have its own type, and returns a value of type R. (Traits are defined for N = 0 to 22.) The variance annotations (‘+’ and ‘-’) in front of the types will be explained in the section called “Variance Under Inheritance” in Chapter 12, The Scala Type System.

Iterable[+T]

AnyRef

Trait with methods for operating on collections of instances. Users implement the abstract elements method to return an Iterable instance.

List[+T]

Seq[T]

sealed abstract class for ordered collections with functional-style list semantics. It is the most widely-used collection in Scala, so it is defined in the scala package, rather than one of the collection packages. (In Scala version2.8, it is actually defined in scala.collection.immutable and “aliased” in package object scala). It has two subclasses, case object Nil, which extends List[Nothing] and represents an empty list, and case final class ::[T], which represents a non-empty list, characterized by a head element and a tail list, which would be Nil for a one-element list.

Nothing

all other types

Nothing is the subtype of all other types. It has no instances. It is used primarily for defining other types in a type-safe way, such as the special List subtype Nil. See also the section called “Nothing and Null” in Chapter 12, The Scala Type System.

Null

all reference types

Null has one instance, null, corresponding to the runtime’s concept of null.

Option[T]

Product

Wraps an optional item. It is a sealed abstract type and the only allowed instances are an instance of its derived case class Some[T], wrapping an instance of T, or its derived case object None, which extends Option[Nothing].

Predef

AnyRef

An object that defines and imports many commonly-used types and methods. See the section called “The Predef Object” earlier in this chapter for details.

Product

AnyRef

Trait with methods for determining arity and getting the nth item in a “cartesian product”. Subtraits are defined for Product, called ProductN, for dimension N from 1 through 22.

ScalaObject

AnyRef

Mixin trait added to all Scala reference type instances.

Seq[+T]

Collection[T]

Trait for ordered collections.

TupleN

ProductN

Separate case classes for arity N = 1 through 22. Tuples support the literal syntax (x1, x2, …, xN).


2 comments

  1. Esa Laine Posted 1 month, 20 days and 6 hours ago

    The second line for the description for List[+T] has a typo. scala pacakge -> scala package

  2. Dean Wampler Posted 1 month, 6 days and 14 hours ago

    Fixed. Thx.

Add a comment

Besides List, some of the other library collections include Map, Set, Queue, and Stack. These other collections come in two varieties, mutable and immutable. The immutable collections are in the package scala.collection.immutable, while the mutable collections are in scala.collection.mutable. Only an immutable version of List is provided; for a mutable list, use a ListBuffer, which can return a List via the toList method. For Scala version 2.8, the collections implementations reuse code from scala.collection.generic. Users of the collections would normally not use any types defined in this package. We’ll explore some of these collections in greater detail in the section called “Functional Data Structures” in Chapter 8, Functional Programming in Scala.

Consistent with its emphasis on functional programming (see Chapter 8, Functional Programming in Scala), Scala encourages you to use the immutable collections, since List is automatically imported and Predef defines types Map and Set that refer to the immutable versions of these collections. All other collections have to be imported explicitly.

Predef defines a number of implicit conversion methods for the value types (excluding Unit). There are implicit conversions to the corresponding scala.runtime.RichX types. For example, the byteWrapper method converts a Byte to a scala.runtime.RichByte. There are implicit conversions between the “numeric” types, Byte, Short, Int, Long, and Float to the other types that are “wider” than the original. For example, Byte to Int, Int to Long, Int, to Double, etc. Finally, there are conversions to the corresponding Java wrapper types, e.g., Int to java.lang.Integer. We discuss implicit conversions in more detail in the section called “Implicit Conversions” in Chapter 8, Functional Programming in Scala.

There are several examples of Option elsewhere, e.g., the section called “Option, Some, and None: Avoiding nulls” in Chapter 2, Type Less, Do More. Here is a script that illustrates using an Either return value to handle a thrown exception or successful result (adapted from http://dcsobral.blogspot.com/2009/06/catching-exceptions.html).

// code-examples/ObjectSystem/typehierarchy/either-script.scala

def exceptionToLeft[T](f: => T): Either[java.lang.Throwable, T] = try {
  Right(f)
} catch {
  case ex => Left(ex)
}

def throwsOnOddInt(i: Int) = i % 2 match {
  case 0 => i
  case 1 => throw new RuntimeException(i + " is odd!")
}

for(i <- 0 to 3)
  exceptionToLeft(throwsOnOddInt(i)) match {
    case Left(ex) => println("Oops, got exception " + ex.toString)
    case Right(x) => println(x)
  }

The exceptionToLeft method evaluates f. It catches a Throwable and returns it as the Left value or returns the normal result as the Right value. The for loop uses this method to invoke throwsOnOddInt. It pattern matches on the result and prints an appropriate message. The output of the script is the following.

0
Oops, got exception java.lang.RuntimeException: 1 is odd!
2
Oops, got exception java.lang.RuntimeException: 3 is odd!

A FunctionN trait, where N is 0 to 22, is instantiated for an anonymous function with N arguments. So, consider the following anonymous function.

(t1: T1, ..., tN: TN) => new R(...)

It is syntactic sugar for the following creation of an anonymous class.

new FunctionN {
  def apply(t1: T1, ..., tN: TN): R = new R(...)

  // other methods
}

We’ll revisit FunctionN in the section called “Variance Under Inheritance” and the section called “Function Types” in Chapter 12, The Scala Type System.

Linearization of an Object’s Hierarchy

Because of single inheritance, the inheritance hierarchy would be linear, if we ignored mixed-in traits. When traits are considered, each of which may be derived from other traits and classes, the inheritance hierarchy forms a directed, acyclic graph [ScalaSpec2009]. The term linearization refers to the algorithm used to “flatten” this graph for the purposes of resolving method lookup priorities, constructor invocation order, binding of super, etc.

Informally, we saw in the section called “Stackable Traits” in Chapter 4, Traits that when an instance has more than one trait, they bind right to left, as declared. Consider the following example of linearization.

// code-examples/ObjectSystem/linearization/linearization1-script.scala

class C1 {
  def m = List("C1")
}

trait T1 extends C1 {
  override def m = { "T1" :: super.m }
}

trait T2 extends C1 {
  override def m = { "T2" :: super.m }
}

trait T3 extends C1 {
  override def m = { "T3" :: super.m }
}

class C2 extends T1 with T2 with T3 {
  override def m = { "C2" :: super.m }
}

val c2 = new C2
println(c2.m)

Running this script yields the following output.

List(C2, T3, T2, T1, C1)

This list of strings built up by the m methods reflects the linearization of the inheritance hierarchy, with a few missing pieces we’ll discuss shortly. We’ll also see why C1 is at the end of the list. First, let’s see what the invocation sequence of the constructors looks like.

// code-examples/ObjectSystem/linearization/linearization2-script.scala

var clist = List[String]()

class C1 {
  clist ::= "C1"
}

trait T1 extends C1 {
  clist ::= "T1"
}

trait T2 extends C1 {
  clist ::= "T2"
}

trait T3 extends C1 {
  clist ::= "T3"
}

class C2 extends T1 with T2 with T3 {
  clist ::= "C2"
}

val c2 = new C2
println(clist.reverse)

Running this script yields the following output.

List(C1, T1, T2, T3, C2)

So, the construction sequence is the reverse. (We had to reverse the list on the last line, because the way it was constructed put the elements in the reverse order.) This invocation order makes sense. For proper construction to occur, the parent types need to be constructed before the derived types, since a derived type often uses fields and methods in the parent types during its construction process.

The output of the first linearization script is actually missing three types at the end. The full linearization for reference types actually ends with ScalaObject, AnyRef, and Any. So the linearization for C2 is actually.

List(C2, T3, T2, T1, C1, ScalaObject, AnyRef, Any)

Scala inserts the ScalaObject trait as the last mixin, just before AnyRef and Any that are the penultimate and ultimate parent classes of any reference type. Of course, these three types do not show up in the output of the scripts, because we used an ad hoc m method to figure out the behavior by building up an output string.

2 comments

  1. Rob Dickens Posted 1 month, 19 days and 20 hours ago

    we used an our <- ad hoc m method

  2. Dean Wampler Posted 1 month, 6 days and 13 hours ago

    Thx.

Add a comment

The “value types”, subclasses of AnyVal, are all declared abstract final. The compiler manages instantiation of them. Since we can’t subclass them, their linearizations are simple and straightforward.

The linearization defines the order in which method look-up occurs. Let’s examine it more closely.

All our classes and traits define the method m. The one in C3 is called first, since the instance is of that type. C3.m calls super.m, which resolves to T3.m. The search appears to be “breadth-first”, rather than “depth-first”. (If it were depth-first, it would invoke C1.m after T3.m.) After, T3.m, T2.m, then T1.m, and finally C1.m are invoked. C1 is the parent of the three traits. From which of the traits did we traverse to C1? Let’s modify our first example and see how we got to C1.

1 comment

  1. Gleb Frank Posted 25 days and 11 hours ago

    Saying it's breadth-first is misleading, IMHO. It is actually depth first, with a twist: it leaves off handling each node for as late as possible. So, you could say that it does examine C1 after T3, but does not handle it, since it will have the opportunity to visit it from T1 later.

    The distinction is very clear if you insert an additional layer of traits, T1B, T2B and T3B, between T1-3 and C2 (meaning, T1B extends T1, etc., and C2 extends T1B with T2B with T3B. Then the order of invocation will be C2, T3B, T3, T2B, T2, T1B, T1, C1. So, basically it's depth first, with the modification as noted above.

    I know you specify the full algorithm below, but you never repudiate the "breadth-first" claim, and some people may be left with a misconception.

Add a comment

// code-examples/ObjectSystem/linearization/linearization3-script.scala

class C1 {
  def m(previous: String) = List("C1("+previous+")")
}

trait T1 extends C1 {
  override def m(p: String) = { "T1" :: super.m("T1") }
}

trait T2 extends C1 {
  override def m(p: String) = { "T2" :: super.m("T2") }
}

trait T3 extends C1 {
  override def m(p: String) = { "T3" :: super.m("T3") }
}

class C2 extends T1 with T2 with T3 {
  override def m(p: String) = { "C2" :: super.m("C2") }
}

val c2 = new C2
println(c2.m(""))

Now we pass the name of the caller of super.m as a parameter, then C1 prints out who called it. Running this script yields the following output.

List(C2, T3, T2, T1, C1(T1))

It’s the last one, T1. We might have expected T3 from a “naïve” application of breadth-first traversal.

Here is the actual algorithm for calculating the linearization. A more formal definition is given in [ScalaSpec2009].

This explains how we got to C1 from T1 in the previous example. T3 and T2 also have it in their linearizations, but they come before T1, so the C1 terms they contributed were deleted.

Let’s work through the algorithm using a slightly more involved example.

// code-examples/ObjectSystem/linearization/linearization4-script.scala

class C1 {
  def m = List("C1")
}

trait T1 extends C1 {
  override def m = { "T1" :: super.m }
}

trait T2 extends C1 {
  override def m = { "T2" :: super.m }
}

trait T3 extends C1 {
  override def m = { "T3" :: super.m }
}

class C2A extends T2 {
  override def m = { "C2A" :: super.m }
}

class C2 extends C2A with T1 with T2 with T3 {
  override def m = { "C2" :: super.m }
}

def calcLinearization(obj: C1, name: String) = {
  val lin = obj.m ::: List("ScalaObject", "AnyRef", "Any")
  println(name + ":  " + lin)
}

calcLinearization(new C2, "C2 ")
println("")
calcLinearization(new T3 {}, "T3 ")
calcLinearization(new T2 {}, "T2 ")
calcLinearization(new T1 {}, "T1 ")
calcLinearization(new C2A, "C2A")
calcLinearization(new C1, "C1 ")

The output is the following.

C2 :  List(C2, T3, T1, C2A, T2, C1, ScalaObject, AnyRef, Any)

T3 :  List(T3, C1, ScalaObject, AnyRef, Any)
T2 :  List(T2, C1, ScalaObject, AnyRef, Any)
T1 :  List(T1, C1, ScalaObject, AnyRef, Any)
C2A:  List(C2A, T2, C1, ScalaObject, AnyRef, Any)
C1 :  List(C1, ScalaObject, AnyRef, Any)

To help us along, we calculated the linearizations for the other types and we also appended ScalaObject, AnyRef, and Any to remind ourselves that they should also be there. We also removed the logic to pass the caller’s name to m. That caller of C1 will always be the element to its immediate left.

So, let’s work through the algorithm for C2 and confirm our results. We’ll suppress the ScalaObject, AnyRef, and Any for clarity, until the end.

Table 7.5. Hand Calculation of C2 linearization: C2 extends C2A with T1 with T2 with T3 {…}.

# Linearization Description

1

C2

Add the type of the instance.

2

C2, T3, C1

Add the linearization for T3 (farthest on the right).

3

C2, T3, C1, T2, C1

Add the linearization for T2.

4

C2, T3, C1, T2, C1, T1, C1

Add the linearization for T1.

5

C2, T3, C1, T2, C1, T1, C1, C2A, T2, C1

Add the linearization for C2A.

6

C2, T3, T2, T1, C2A, T2, C1

Remove duplicates of C1; all but the last C1.

7

C2, T3, T1, C2A, T2, C1

Remove duplicate T2; all but the last T2.

8

C2, T3, T1, C2A, T2, C1, ScalaObject, AnyRef, Any

Finish!


What the algorithm does is push any shared types to the right until they come after all the types that derive from them.

Try modifying the last script with different hierarchies and see if you can reproduce the results using the algorithm.

Tip

Overly complex type hierarchies can result in method lookup “surprises”. If you have to work through this algorithm to figure out what’s going on, try to simplify your code.

Recap and What’s Next

We have finished our survey of Scala’s object model. If you come from an object-oriented language background, you now know enough about Scala to replace your existing object-oriented language with object-oriented Scala.

However, there is much more to come. Scala supports functional programming, which offers powerful mechanisms for addressing a number of design problems, such as concurrency. We’ll see that functional programming appears to contradict object-oriented programming, at least on the surface. That said, a guiding principle behind Scala is that these two paradigms complement each other more than they conflict. Combined, they give you more options for building robust, scalable software. Scala lets you choose the techniques that work best for your needs.

You must sign in or register before commenting