The Artima Developer Community

Chapter 20 of Programming in Scala, First Edition
Abstract Members
by Martin Odersky, Lex Spoon, and Bill Venners
December 10, 2008

A member of a class or trait is abstract if the member does not have a complete definition in the class. Abstract members are intended to be implemented in subclasses of the class in which they are declared. This idea is found in many object-oriented languages. For instance, Java lets you declare abstract methods. Scala also lets you declare such methods, as you saw in Section 10.2. But Scala goes beyond that and implements the idea in its full generality: besides methods, you can also declare abstract fields and even abstract types as members of classes and traits.

In this chapter we'll describe all four kinds of abstract member: vals, vars, methods, and types. Along the way we'll discuss pre-initialized fields, lazy vals, path-dependent types, and enumerations.

20.1 A quick tour of abstract members [link]

The following trait declares one of each kind of abstract member: an abstract type (T), method (transform), val (initial), and var (current):

  trait Abstract {
    type T
    def transform(x: T): T
    val initial: T
    var current: T
  }
A concrete implementation of Abstract needs to fill in definitions for each of its abstract members. Here is an example implementation that provides these definitions:
  class Concrete extends Abstract {
    type T = String
    def transform(x: String) = x + x
    val initial = "hi"
    var current = initial
  }
The implementation gives a concrete meaning to the type name T by defining it as an alias of type String. The transform operation concatenates a given string with itself, and the initial and current values are both set to "hi".

This example gives you a rough first idea of what kinds of abstract members exist in Scala. The remainder of the chapter will present the details and explain what the new forms of abstract members, as well as type members in general, are good for.

20.2 Type members [link]

As you can see from the example in the previous section, the term abstract type in Scala means a type declared (with the "type" keyword) to be a member of a class or trait, without specifying a definition. Classes themselves may be abstract, and traits are by definition abstract, but neither of these are what are referred to as abstract types in Scala. An abstract type in Scala is always a member of some class or trait, such as type T in trait Abstract.

You can think of a non-abstract (or, "concrete") type member, such as type T in class Concrete, as a way to define a new name, or alias, for a type. In class Concrete, for example, the type String is given the alias T. As a result, anywhere T appears in the definition of class Concrete, it means String. This includes the parameter and result types of transform, initial, and current, which mention T when they are declared in supertrait Abstract. Thus, when class Concrete implements these methods, those Ts are interpreted to mean String.

One reason to use a type member is to define a short, descriptive alias for a type whose real name is more verbose, or less obvious in meaning, than the alias. Such type members can help clarify the code of a class or trait. The other main use of type members is to declare abstract types that must be defined in subclasses. This use, which was demonstrated in the previous section, will be described in detail later in this chapter.

20.3 Abstract vals [link]

An abstract val declaration has a form like:

  val initial: String
It gives a name and type for a val, but not its value. This value has to be provided by a concrete val definition in a subclass. For instance, class Concrete implemented the val using:
  val initial = "hi"
You use an abstract val declaration in a class when you do not know the correct value in the class, but you do know that the variable will have an unchangeable value in each instance of the class.

An abstract val declaration resembles an abstract parameterless method declaration such as:

  def initial: String
Client code would refer to both the val and the method in exactly the same way, i.e., obj.initial. However, if initial is an abstract val, the client is guaranteed that obj.initial will yield the same value every time it is referenced. If initial were an abstract method, that guarantee would not hold, because in that case initial could be implemented by a concrete method that returns a different value every time it's called.

In other words, an abstract val constrains its legal implementation: any implementation must be a val definition; it may not be a var or a def. Abstract method declarations, on the other hand, may be implemented by both concrete method definitions and concrete val definitions. Given the abstract class Fruit shown in Listing 20.1, class Apple would be a legal subclass implementation, but class BadApple would not.

  abstract class Fruit {
    val v: String // `v' for value
    def m: String // `m' for method
  }
  
abstract class Apple extends Fruit {   val v: String   val m: String // OK to override a `def' with a `val' }
abstract class BadApple extends Fruit {   def v: String // ERROR: cannot override a `val' with a `def'   def m: String }
Listing 20.1 - Overriding abstract vals and parameterless methods.

20.4 Abstract vars [link]

Like an abstract val, an abstract var declares just a name and a type, but not an initial value. For instance, Listing 20.2 shows a trait AbstractTime, which declares two abstract variables named hour and minute:

    trait AbstractTime {
      var hour: Int
      var minute: Int
    }
Listing 20.2 - Declaring abstract vars.

What is the meaning of abstract vars like hour and minute? You saw in Section 18.2 that vars declared as members of classes come equipped with getter and setter methods. This holds for abstract vars as well. If you declare an abstract var named hour, for example, you implicitly declare an abstract getter method, hour, and an abstract setter method, hour_=. There's no reassignable field to be defined—that will come in subclasses that define the concrete implementation of the abstract var. For instance, the definition of AbstractTime shown in Listing 20.2 is exactly equivalent to the definition shown in Listing 20.3.

    trait AbstractTime {
      def hour: Int          // getter for `hour'
      def hour_=(x: Int)     // setter for `hour'
      def minute: Int        // getter for `minute'
      def minute_=(x: Int)   // setter for `minute'
    }
Listing 20.3 - How abstract vars are expanded into getters and setters.

20.5 Initializing abstract vals [link]

Abstract vals sometimes play a role analogous to superclass parameters: they let you provide details in a subclass that are missing in a superclass. This is particularly important for traits, because traits don't have a constructor to which you could pass parameters. So the usual notion of parameterizing a trait works via abstract vals that are implemented in subclasses. As an example, consider a reformulation of class Rational from Chapter 6, as shown in Listing 6.5 here, as a trait:

  trait RationalTrait { 
    val numerArg: Int 
    val denomArg: Int 
  } 
The Rational class from Chapter 6 had two parameters: n for the numerator of the rational number, and d for the denominator. The RationalTrait trait given here defines instead two abstract vals: numerArg and denomArg. To instantiate a concrete instance of that trait, you need to implement the abstract val definitions. Here's an example:
  new RationalTrait {
    val numerArg = 1
    val denomArg = 2
  }
Here the keyword new appears in front of a trait name, RationalTrait, which is followed by a class body in curly braces. This expression yields an instance of an anonymous class that mixes in the trait and is defined by the body. This particular anonymous class instantiation has an effect analogous to the instance creation new Rational(1, 2). The analogy is not perfect, however. There's a subtle difference concerning the order in which expressions are initialized. When you write:
  new Rational(expr1, expr2)
the two expressions, expr1 and expr2, are evaluated before class Rational is initialized, so the values of expr1 and expr2 are available for the initialization of class Rational. For traits, however, the situation is the opposite. When you write:
  new RationalTrait {
    val numerArg = expr1
    val denomArg = expr2
  }
the expressions, expr1 and expr2, are evaluated as part of the initialization of the anonymous class, but the anonymous class is initialized after the RationalTrait. So the values of numerArg and denomArg are not available during the initialization of RationalTrait (more precisely, a selection of either value would yield the default value for type Int, 0). For the definition of RationalTrait given previously, this is not a problem, because the trait's initialization does not make use of values numerArg or denomArg. However, it does become a problem in the variant of RationalTrait shown in Listing 20.4, which defines normalized numerators and denominators:

    trait RationalTrait { 
      val numerArg: Int 
      val denomArg: Int 
      require(denomArg != 0)
      private val g = gcd(numerArg, denomArg)
      val numer = numerArg / g
      val denom = denomArg / g
      private def gcd(a: Int, b: Int): Int = 
        if (b == 0) a else gcd(b, a % b)
      override def toString = numer +"/"+ denom
    }
Listing 20.4 - A trait that uses its abstract vals.

If you try to instantiate this trait with some numerator and denominator expressions that are not simple literals, you'll get an exception:

  scala> val x = 2
  x: Int = 2
  
scala> new RationalTrait {          val numerArg = 1 * x          val denomArg = 2 * x        } java.lang.IllegalArgumentException: requirement failed         at scala.Predef$.require(Predef.scala:107)         at RationalTrait$class.$init$(<console>:7)         at $anon$1.<init>(<console>:7)         ....
The exception in this example was thrown because denomArg still had its default value of 0 when class RationalTrait was initialized, which caused the require invocation to fail.

This example demonstrates that initialization order is not the same for class parameters and abstract fields. A class parameter argument is evaluated before it is passed to the class constructor (unless the parameter is by-name). An implementing val definition in a subclass, by contrast, is evaluated only after the superclass has been initialized.

Now that you understand why abstract vals behave differently from parameters, it would be good to know what can be done about this. Is it possible to define a RationalTrait that can be initialized robustly, without fearing errors due to uninitialized fields? In fact, Scala offers two alternative solutions to this problem, pre-initialized fields and lazy vals. They are presented in the remainder of this section.

Pre-initialized fields

The first solution, pre-initialized fields, lets you initialize a field of a subclass before the superclass is called. To do this, simply place the field definition in braces before the superclass constructor call. As an example, Listing 20.5 shows another attempt to create an instance of RationalTrait. As you see from this example, the initialization section comes before the mention of the supertrait RationalTrait. Both are separated by a with.

    scala> new { 
             val numerArg = 1 * x
             val denomArg = 2 * x 
           } with RationalTrait
    res15: java.lang.Object with RationalTrait = 1/2
Listing 20.5 - Pre-initialized fields in an anonymous class expression.

    object twoThirds extends {
      val numerArg = 2
      val denomArg = 3
    } with RationalTrait
Listing 20.6 - Pre-initialized fields in an object definition.

Pre-initialized fields are not restricted to anonymous classes; they can also be used in objects or named subclasses. Two examples are shown in Listings 20.6 and 20.7. As you can see from these examples, the pre-initialization section comes in each case after the extends keyword of the defined object or class. Class RationalClass, shown in Listing 20.7, exemplifies a general schema of how class parameters can be made available for the initialization of a supertrait.

Because pre-initialized fields are initialized before the superclass constructor is called, their initializers cannot refer to the object that's being constructed. Consequently, if such an initializer refers to this, the reference goes to the object containing the class or object that's being constructed, not the constructed object itself. Here's an example:

  scala> new {
         val numerArg = 1
         val denomArg = this.numerArg * 2
       } with RationalTrait
  <console>:8: error: value numerArg is not a
       member of object $iw
           val denomArg = this.numerArg * 2
                               ^

    class RationalClass(n: Int, d: Intextends {
      val numerArg = n
      val denomArg = d
    } with RationalTrait {
      def + (that: RationalClass) = new RationalClass(
        numer * that.denom + that.numer * denom,
        denom * that.denom
      )
    }
Listing 20.7 - Pre-initialized fields in a class definition.

The example did not compile because the reference this.numerArg was looking for a numerArg field in the object containing the new (which in this case was the synthetic object named $iw, into which the interpreter puts user input lines). Once more, pre-initialized fields behave in this respect like class constructor arguments.

Lazy vals

You can use pre-initialized fields to simulate precisely the initialization behavior of class constructor arguments. Sometimes, however, you might prefer to let the system itself sort out how things should be initialized. This can be achieved by making your val definitions lazy. If you prefix a val definition with a lazy modifier, the initializing expression on the right-hand side will only be evaluated the first time the val is used.

For an example, define an object Demo with a val as follows:

  scala> object Demo {
           val x = { println("initializing x"); "done" }
         }
  defined module Demo
Now, first refer to Demo, then to Demo.x:
  scala> Demo
  initializing x
  res19: Demo.type = Demo$@97d1ff
  
scala> Demo.x res20: java.lang.String = done
As you can see, the moment you use Demo, its x field becomes initialized. The initialization of x forms part of the initialization of Demo. The situation changes, however, if you define the x field to be lazy:
  scala> object Demo {
           lazy val x = { println("initializing x"); "done" }
         }
  defined module Demo
  
scala> Demo res21: Demo.type = Demo$@d81341
scala> Demo.x initializing x res22: java.lang.String = done
Now, initializing Demo does not involve initializing x. The initialization of x will be deferred until the first time x is used.

    trait LazyRationalTrait { 
      val numerArg: Int 
      val denomArg: Int 
      lazy val numer = numerArg / g
      lazy val denom = denomArg / g
      override def toString = numer +"/"+ denom
      private lazy val g = {
        require(denomArg != 0)
        gcd(numerArg, denomArg)
      }
      private def gcd(a: Int, b: Int): Int = 
        if (b == 0) a else gcd(b, a % b)
    }
Listing 20.8 - Initializing a trait with lazy vals.

This is similar to the situation where x is defined as a parameterless method, using a def. However, unlike a def a lazy val is never evaluated more than once. In fact, after the first evaluation of a lazy val the result of the evaluation is stored, to be reused when the same val is used subsequently.

Looking at this example, it seems that objects like Demo themselves behave like lazy vals, in that they are also initialized on demand, the first time they are used. This is correct. In fact an object definition can be seen as a shorthand for the definition of a lazy val with an anonymous class that describes the object's contents.

Using lazy vals, you could reformulate RationalTrait as shown in Listing 20.8. In the new trait definition, all concrete fields are defined lazy. Another change with respect to the previous definition of RationalTrait, shown in Listing 20.4, is that the require clause was moved from the body of the trait to the initializer of the private field, g, which computes the greatest common divisor of numerArg and denomArg. With these changes, there's nothing that remains to be done when LazyRationalTrait is initialized; all initialization code is now part of the right-hand side of a lazy val. Therefore, it is safe to initialize the abstract fields of LazyRationalTrait after the class is defined. Here's an example:

  scala> val x = 2
  x: Int = 2
  
scala> new LazyRationalTrait {          val numerArg = 1 * x          val denomArg = 2 * x        } res1: java.lang.Object with LazyRationalTrait = 1/2
No pre-initialization is needed. It's instructive to trace the sequence of initializations that lead to the string 1/2 to be printed in the code above:

  1. First, a fresh instance of LazyRationalTrait gets created, and the initialization code of LazyRationalTrait is run. This initialization code is empty—none of the fields of LazyRationalTrait is as yet initialized.
  2. Next, the primary constructor of the anonymous subclass defined by the new expression is executed. This involves the initialization of numerArg with 2 and denomArg with 4.
  3. Next, the toString method is invoked on the constructed object by the interpreter, so that the resulting value can be printed.
  4. Next, the numer field is accessed for the first time by the toString method in trait LazyRationalTrait, so its initializer is evaluated.
  5. The initializer of numer accesses the private field, g, so g is evaluated next. This evaluation accesses numerArg and denomArg, which were defined in Step 2.
  6. Next, the toString method accesses the value of denom, which causes denom's evaluation. The evaluation of denom accesses the values of denomArg and g. The initializer of the g field is not re-evaluated, because it was already evaluated in Step 5.
  7. Finally, the result string "1/2" is constructed and printed.
Note that the definition of g comes textually after the definitions of numer and denom in class LazyRationalTrait. Nevertheless, because all three values are lazy, g gets initialized before the initialization of numer and denom is completed. This shows an important property of lazy vals: the textual order of their definitions does not matter, because values get initialized on demand. Therefore, lazy vals can free you as a programmer from having to think hard how to arrange val definitions to ensure that everything is defined when it is needed.

However, this advantage holds only as long as the initialization of lazy vals neither produces side effects nor depends on them. In the presence of side effects, initialization order starts to matter. And then it can be quite difficult to trace in what order initialization code is run, as the previous example has demonstrated. So lazy vals are an ideal complement to functional objects, where the order of initializations does not matter, as long as everything gets initialized eventually. They are less well suited for code that's predominantly imperative.

Lazy functional languages

Scala is by no means the first language to have exploited the perfect match of lazy definitions and functional code. In fact, there is a category of "lazy functional programming languages" in which every value and parameter is initialized lazily. The best known member of this class of languages is Haskell haskell98.

20.6 Abstract types [link]

In the beginning of this chapter, you saw, "type T", an abstract type declaration. The rest of this chapter discusses what such an abstract type declaration means and what it's good for. Like all other abstract declarations, an abstract type declaration is a placeholder for something that will be defined concretely in subclasses. In this case, it is a type that will be defined further down the class hierarchy. So T above refers to a type that is at yet unknown at the point where it is declared. Different subclasses can provide different realizations of T.

Here is a well-known example where abstract types show up naturally. Suppose you are given the task of modeling the eating habits of animals. You might start with a class Food and a class Animal with an eat method:

  class Food
  abstract class Animal {
    def eat(food: Food)
  }
You might then attempt to specialize these two classes to a class of Cows that eat Grass:
  class Grass extends Food
  class Cow extends Animal {
    override def eat(food: Grass) {} // This won't compile
  }
However, if you tried to compile the new classes, you'd get the following compilation errors:

  BuggyAnimals.scala:7: error: class Cow needs to be
  abstract, since method eat in class Animal of type
      (Food)Unit is not defined
  class Cow extends Animal {
        ^
  BuggyAnimals.scala:8: error: method eat overrides nothing
    override def eat(food: Grass) {}
                 ^

What happened is that the eat method in class Cow does not override the eat method in class Animal, because its parameter type is different—it's Grass in class Cow vs. Food in class Animal.

Some people have argued that the type system is unnecessarily strict in refusing these classes. They have said that it should be OK to specialize a parameter of a method in a subclass. However, if the classes were allowed as written, you could get yourself in unsafe situations very quickly. For instance, the following script would pass the type checker:

  class Food
  abstract class Animal {
    def eat(food: Food)
  }
  class Grass extends Food
  class Cow extends Animal {
    override def eat(food: Grass) {} // This won't compile,
  }                                  // but if it did,...
  class Fish extends Food
  val bessy: Animal = new Cow
  bessy eat (new Fish)     // ...you could feed fish to cows.
The program would compile if the restriction were eased, because Cows are Animals and Animals do have an eat method that accepts any kind of Food, including Fish. But surely it would do a cow no good to eat a fish!

What you need to do instead is apply some more precise modeling. Animals do eat Food, but what kind of Food each Animal eats depends on the Animal. This can be neatly expressed with an abstract type, as shown in Listing 20.9:

    class Food
    abstract class Animal {
      type SuitableFood <: Food
      def eat(food: SuitableFood)
    }
Listing 20.9 - Modeling suitable food with an abstract type.

With the new class definition, an Animal can eat only food that's suitable. What food is suitable cannot be determined at the level of the Animal class. That's why SuitableFood is modeled as an abstract type. The type has an upper bound, Food, which is expressed by the "<: Food" clause. This means that any concrete instantiation of SuitableFood (in a subclass of Animal) must be a subclass of Food. For example, you would not be able to instantiate SuitableFood with class IOException.

    class Grass extends Food
    class Cow extends Animal {
      type SuitableFood = Grass
      override def eat(food: Grass) {}
    }
Listing 20.10 - Implementing an abstract type in a subclass.

With Animal defined, you can now progress to cows, as shown in Listing 20.10. Class Cow fixes its SuitableFood to be Grass and also defines a concrete eat method for this kind of food. These new class definitions compile without errors. If you tried to run the "cows-that-eat-fish" counterexample with the new class definitions, you would get the following compiler error:

  scala> class Fish extends Food
  defined class Fish
  
scala> val bessy: Animal = new Cow bessy: Animal = Cow@674bf6
scala> bessy eat (new Fish) <console>:10: error: type mismatch;  found   : Fish  required: bessy.SuitableFood        bessy eat (new Fish)                   ^

20.7 Path-dependent types [link]

Have a look at the last error message: What's interesting about it is the type required by the eat method: bessy.SuitableFood. This type consists of an object reference, bessy, which is followed by a type field, SuitableFood, of the object. So this shows that objects in Scala can have types as members. The meaning of bessy.SuitableFood is "the type SuitableFood that is a member of the object referenced from bessy," or alternatively, the type of food that's suitable for bessy. A type like bessy.SuitableFood is called a path-dependent type. The word "path" here means a reference to an object. It could be a single name, such as bessy, or a longer access path, such as farm.barn.bessy.SuitableFood, where each of farm, barn, and bessy are variables (or singleton object names) that refer to objects.

As the term "path-dependent type" says, the type depends on the path: in general, different paths give rise to different types. For instance, say you defined classes DogFood and Dog, like this:

  class DogFood extends Food
  class Dog extends Animal {
    type SuitableFood = DogFood
    override def eat(food: DogFood) {}
  }
If you attempted to feed a dog with food fit for a cow, your code would not compile:
  scala> val bessy = new Cow
  bessy: Cow = Cow@10cd6d
  
scala> val lassie = new Dog bootsie: Dog = Dog@d11fa6
scala> lassie eat (new bessy.SuitableFood) <console>:13: error: type mismatch;  found   : Grass  required: DogFood        lassie eat (new bessy.SuitableFood)                    ^

The problem here is that the type of the SuitableFood object passed to the eat method, bessy.SuitableFood, is incompatible with the parameter type of eat, lassie.SuitableFood. The case would be different for two Dogs however. Because Dog's SuitableFood type is defined to be an alias for class DogFood, the SuitableFood types of two Dogs are in fact the same. As a result, the Dog instance named lassie could actually eat the suitable food of a different Dog instance (which we'll name bootsie):

  scala> val bootsie = new Dog
  bootsie: Dog = Dog@54ca71
  
scala> lassie eat (new bootsie.SuitableFood)

A path-dependent type resembles the syntax for an inner class type in Java, but there is a crucial difference: a path-dependent type names an outer object, whereas an inner class type names an outer class. Java-style inner class types can also be expressed in Scala, but they are written differently. Consider these two classes, Outer and Inner:

  class Outer {
    class Inner
  }
In Scala, the inner class is addressed using the expression Outer#Inner instead of Java's Outer.Inner. The `.' syntax is reserved for objects. For example, imagine you instantiate two objects of type Outer, like this:
  val o1 = new Outer
  val o2 = new Outer
Here o1.Inner and o2.Inner are two path-dependent types (and they are different types). Both of these types conform to (are subtypes of) the more general type Outer#Inner, which represents the Inner class with an arbitrary outer object of type Outer. By contrast, type o1.Inner refers to the Inner class with a specific outer object (the one referenced from o1). Likewise, type o2.Inner refers to the Inner class with a different, specific outer object (the one referenced from o2).

In Scala, as in Java, inner class instances hold a reference to an enclosing outer class instance. This allows an inner class, for example, to access members of its outer class. Thus you can't instantiate an inner class without in some way specifying an outer class instance. One way to do this is to instantiate the inner class inside the body of the outer class. In this case, the current outer class instance (referenced from this) will be used. Another way is to use a path-dependent type. For example, because the type, o1.Inner, names a specific outer object, you can instantiate it:

  scala> new o1.Inner
  res1: o1.Inner = Outer$Inner@13727f
The resulting inner object will contain a reference to its outer object, the object referenced from o1. By contrast, because the type Outer#Inner does not name any specific instance of Outer, you can't create an instance of it:
  scala> new Outer#Inner
  <console>:6: error: Outer is not a legal prefix for
    a constructor
         new Outer#Inner
                   ^

20.8 Enumerations [link]

An interesting application of path-dependent types is found in Scala's support for enumerations. Some other languages, including Java and C#, have enumerations as a built-in language construct to define new types. Scala does not need special syntax for enumerations. Instead, there's a class in its standard library, scala.Enumeration. To create a new enumeration, you define an object that extends this class, as in the following example, which defines a new enumeration of Colors:

  object Color extends Enumeration {
    val Red = Value
    val Green = Value
    val Blue = Value
  }
Scala lets you also shorten several successive val or var definitions with the same right-hand side. Equivalently to the above you could write:
  object Color extends Enumeration {
    val RedGreenBlue = Value
  }
This object definition provides three values: Color.Red, Color.Green, and Color.Blue. You could also import everything in Color with:
  import Color._
and then just use Red, Green, and Blue. But what is the type of these values? Enumeration defines an inner class named Value, and the same-named parameterless Value method returns a fresh instance of that class. This means that a value such as Color.Red is of type Color.Value. Color.Value is the type of all enumeration values defined in object Color. It's a path-dependent type, with Color being the path and Value being the dependent type. What's significant about this is that it is a completely new type, different from all other types. In particular, if you would define another enumeration, such as:
  object Direction extends Enumeration {
    val NorthEastSouthWest = Value
  }
then Direction.Value would be different from Color.Value because the path parts of the two types differ.

Scala's Enumeration class also offers many other features found in the enumeration designs of other languages. You can associate names with enumeration values by using a different overloaded variant of the Value method:

  object Direction extends Enumeration {
    val North = Value("North")
    val East = Value("East")
    val South = Value("South")
    val West = Value("West")
  }
You can step through all values of an enumeration with foreach, or use for expressions with map, flatMap and filter:
  scala> for (d <- Direction) print(d +" ")
  North East South West 
Values of an enumeration are numbered from 0, and you can find out the number of an enumeration value by its id method:
  scala> Direction.East.id
  res5: Int = 1
It's also possible to go the other way, from a non-negative integer number to the value that has this number as id in an enumeration:
  scala> Direction(1)
  res6: Direction.Value = East
This should be enough to get you started with enumerations. You can find more information in the Scaladoc comments of class scala.Enumeration.

20.9 Case study: Currencies [link]

The rest of this chapter presents a case study that explains how abstract types can be used in Scala. The task is to design a class Currency. A typical instance of Currency would represent an amount of money in dollars, euros, yen, or some other currency. It should be possible to do some arithmetic on currencies. For instance, you should be able to add two amounts of the same currency. Or you should be able to multiply a currency amount by a factor representing an interest rate.

These thoughts lead to the following first design for a currency class:

  // A first (faulty) design of the Currency class
  abstract class Currency {
    val amount: Long
    def designation: String 
    override def toString = amount +" "+ designation
    def + (that: Currency): Currency = ...
    def * (x: Double): Currency = ...
  }
The amount of a currency is the number of currency units it represents. This is a field of type Long so that very large amounts of money such as the market capitalization of Google or Microsoft can be represented. It's left abstract here, waiting to be defined when a subclass talks about concrete amounts of money. The designation of a currency is a string that identifies it. The toString method of class Currency indicates an amount and a designation. It would yield results such as:
  79 USD
  11000 Yen
  99 Euro 
Finally, there are methods +, for adding currencies, and *, for multiplying a currency with a floating-point number. You can create a concrete currency value by supplying concrete amount and designation values, like this:
  new Currency {
    val amount = 79L
    def designation = "USD"
  }
This design would be OK if all we wanted to model was a single currency such as only dollars or only euros. But it fails once we need to deal with several currencies. Assume you model dollars and euros as two subclasses of class currency:
  abstract class Dollar extends Currency {
    def designation = "USD"
  }
  abstract class Euro extends Currency {
    def designation = "Euro"
  }
At first glance this looks reasonable. But it would let you add dollars to euros. The result of such an addition would be of type Currency. But it would be a funny currency that was made up of a mix of euros and dollars. What you want instead is a more specialized version of the + method: when implemented in class Dollar, it should take Dollar arguments and yield a Dollar result; when implemented in class Euro, it should take Euro arguments and yield a Euro result. So the type of the addition method would change depending on which class you are in. Nonetheless, you would like to write the addition method just once, not each time a new currency is defined.

In Scala, there's a simple technique to deal with situations like this: if something is not known at the point where a class is defined, make it abstract in the class. This applies to both values and types. In the case of currencies, the exact argument and result type of the addition method are not known, so it is a good candidate for an abstract type. This would lead to the following sketch of class AbstractCurrency:

  // A second (still imperfect) design of the Currency class
  abstract class AbstractCurrency {
    type Currency <: AbstractCurrency
    val amount: Long
    def designation: String 
    override def toString = amount +" "+ designation
    def + (that: Currency): Currency = ...
    def * (x: Double): Currency = ...
  }
The only differences from the previous situation are that the class is now called AbstractCurrency, and that it contains an abstract type Currency, which represents the real currency in question. Each concrete subclass of AbstractCurrency would need to fix the Currency type to refer to the concrete subclass itself, thereby "tying the knot."

For instance, here is a new version of class Dollar, which now extends class AbstractCurrency:

  abstract class Dollar extends AbstractCurrency {
    type Currency = Dollar
    def designation = "USD"
  }
This design is workable, but it is still not perfect. One problem is hidden by the ellipses that indicate the missing method definitions of + and * in class AbstractCurrency. In particular, how should addition be implemented in this class? It's easy enough to calculate the correct amount of the new currency as this.amount + that.amount, but how would you convert the amount into a currency of the right type? You might try something like:
  def + (that: Currency): Currency = new Currency {
    val amount = this.amount + that.amount
  }
However, this would not compile:
  error: class type required
    def + (that: Currency): Currency = new Currency {
                                           ^

One of the restrictions of Scala's treatment of abstract types is that you can neither create an instance of an abstract type, nor have an abstract type as a supertype of another class.[1] So the compiler would refuse the example code above that attempted to instantiate Currency.

However, you can work around this restriction using a factory method. Instead of creating an instance of an abstract type directly, declare an abstract method that does it. Then, wherever the abstract type is fixed to be some concrete type, you also need to give a concrete implementation of the factory method. For class AbstractCurrency, this would look as follows:

  abstract class AbstractCurrency {
    type Currency <: AbstractCurrency // abstract type
    def make(amount: Long): Currency  // factory method
    ...                               // rest of class
  }
A design like this could be made to work, but it looks rather suspicious. Why place the factory method inside class AbstractCurrency? This looks dubious, for at least two reasons. First, if you have some amount of currency (say, one dollar), you also hold in your hand the ability to make more of the same currency, using code such as:
  myDollar.make(100)  // here are a hundred more!
In the age of color copying this might be a tempting scenario, but hopefully not one which you would be able to do for very long without being caught. The second problem with this code is that you can make more Currency objects if you already have a reference to a Currency object, but how do you get the first object of a given Currency? You'd need another creation method, which does essentially the same job as make. So you have a case of code duplication, which is a sure sign of a code smell.

The solution, of course, is to move the abstract type and the factory method outside class AbstractCurrency. You need to create another class that contains the AbstractCurrency class, the Currency type, and the make factory method. We'll call this a CurrencyZone:

  abstract class CurrencyZone {
    type Currency <: AbstractCurrency
    def make(x: Long): Currency
    abstract class AbstractCurrency {
      val amount: Long
      def designation: String 
      override def toString = amount +" "+ designation
      def + (that: Currency): Currency = 
        make(this.amount + that.amount)
      def * (x: Double): Currency = 
        make((this.amount * x).toLong)
    }
  }
An example of a concrete CurrencyZone is the US. You could define this as follows:
  object US extends CurrencyZone {
    abstract class Dollar extends AbstractCurrency {
      def designation = "USD"
    }
    type Currency = Dollar
    def make(x: Long) = new Dollar { val amount = x }
  }
Here, US is an object that extends CurrencyZone. It defines a class Dollar, which is a subclass of AbstractCurrency. So the type of money in this zone is US.Dollar. The US object also fixes the type Currency to be an alias for Dollar, and it gives an implementation of the make factory method to return a dollar amount.

This is a workable design. There are only a few refinements to be added. The first refinement concerns subunits. So far, every currency was measured in a single unit: dollars, euros, or yen. However, most currencies have subunits: for instance, in the US, it's dollars and cents. The most straightforward way to model cents is to have the amount field in US.Currency represent cents instead of dollars. To convert back to dollars, it's useful to introduce a field CurrencyUnit into class CurrencyZone, which contains the amount of one standard unit in that currency:

  class CurrencyZone { 
    ... 
    val CurrencyUnitCurrency 
  } 
The US object could define the quantities Cent, Dollar, and CurrencyUnit as shown in Listing 20.11.

    object US extends CurrencyZone {
      abstract class Dollar extends AbstractCurrency {
        def designation = "USD"
      }
      type Currency = Dollar
      def make(cents: Long) = new Dollar {
        val amount = cents
      }
      val Cent = make(1)
      val Dollar = make(100)
      val CurrencyUnit = Dollar
    }
Listing 20.11 - The US currency zone.

This definition is just like the previous definition of the US object, except that it adds three new fields. The field Cent represents an amount of 1 US.Currency. It's an object analogous to a one-cent coin. The field Dollar represents an amount of 100 US.Currency. So the US object now defines the name Dollar in two ways. The type Dollar (defined by the abstract inner class named Dollar) represents the generic name of the Currency valid in the US currency zone. By contrast, the value Dollar (referenced from the val field named Dollar) represents a single US dollar, analogous to a one-dollar bill. The third field definition of CurrencyUnit specifies that the standard currency unit in the US zone is the Dollar (i.e., the value Dollar, referenced from the field, not the type Dollar).

The toString method in class Currency also needs to be adapted to take subunits into account. For instance, the sum of ten dollars and twenty three cents should print as a decimal number: 10.23 USD. To achieve this, you could implement Currency's toString method as follows:

  override def toString = 
    ((amount.toDouble / CurrencyUnit.amount.toDouble)
     formatted ("%."+ decimals(CurrencyUnit.amount) +"f")
     +" "+ designation)
Here, formatted is a method that Scala makes available on several classes, including Double.[2] The formatted method returns the string that results from formatting the original string on which formatted was invoked according to a format string passed as the formatted method's right-hand operand. The syntax of format strings passed to formatted is the same as that of Java's String.format method. For instance, the format string %.2f formats a number with two decimal digits. The format string used in the toString shown previously is assembled by calling the decimals method on CurrencyUnit.amount. This method returns the number of decimal digits of a decimal power minus one. For instance, decimals(10) is 1, decimals(100) is 2, and so on. The decimals method is implemented by a simple recursion:
  private def decimals(n: Long): Int = 
    if (n == 10 else 1 + decimals(n / 10)
Listing 20.12 shows some other currency zones:

    object Europe extends CurrencyZone {
      abstract class Euro extends AbstractCurrency {
        def designation = "EUR"
      }
      type Currency = Euro
      def make(cents: Long) = new Euro {
        val amount = cents
      }
      val Cent = make(1)
      val Euro = make(100)
      val CurrencyUnit = Euro
    }
  
  object Japan extends CurrencyZone {     abstract class Yen extends AbstractCurrency {       def designation = "JPY"     }     type Currency = Yen     def make(yen: Long) = new Yen {       val amount = yen     }     val Yen = make(1)     val CurrencyUnit = Yen   }
Listing 20.12 - Currency zones for Europe and Japan.

As another refinement you can add a currency conversion feature to the model. As a first step, you could write a Converter object that contains applicable exchange rates between currencies, as shown in Listing 20.13.

    object Converter {
      var exchangeRate = Map(
        "USD" -> Map("USD" -> 1.0   , "EUR" -> 0.7596, 
                     "JPY" -> 1.211 , "CHF" -> 1.223),
        "EUR" -> Map("USD" -> 1.316 , "EUR" -> 1.0   , 
                     "JPY" -> 1.594 , "CHF" -> 1.623),
        "JPY" -> Map("USD" -> 0.8257"EUR" -> 0.6272, 
                     "JPY" -> 1.0   , "CHF" -> 1.018),
        "CHF" -> Map("USD" -> 0.8108"EUR" -> 0.6160, 
                     "JPY" -> 0.982 , "CHF" -> 1.0  )
      )
    }
Listing 20.13 - A converter object with an exchange rates map.

Then, you could add a conversion method, from, to class Currency, which converts from a given source currency into the current Currency object:

  def from(other: CurrencyZone#AbstractCurrency): Currency = 
    make(Math.round(
      other.amount.toDouble * Converter.exchangeRate
        (other.designation)(this.designation)))
The from method takes an arbitrary currency as argument. This is expressed by its formal parameter type, CurrencyZone#AbstractCurrency, which indicates that the argument passed as other must be an AbstractCurrency type in some arbitrary and unknown CurrencyZone. It produces its result by multiplying the amount of the other currency with the exchange rate between the other and the current currency.[3]

The final version of the CurrencyZone class is shown in Listing 20.14.

  abstract class CurrencyZone {
  
  type Currency <: AbstractCurrency   def make(x: Long): Currency
  abstract class AbstractCurrency {
    val amount: Long     def designation: String 
    def + (that: Currency): Currency =        make(this.amount + that.amount)     def * (x: Double): Currency =        make((this.amount * x).toLong)     def - (that: Currency): Currency =        make(this.amount - that.amount)     def / (that: Double) =        make((this.amount / that).toLong)     def / (that: Currency) =        this.amount.toDouble / that.amount
    def from(other: CurrencyZone#AbstractCurrency): Currency =        make(Math.round(         other.amount.toDouble * Converter.exchangeRate           (other.designation)(this.designation)))
    private def decimals(n: Long): Int =        if (n == 10 else 1 + decimals(n / 10)
    override def toString =        ((amount.toDouble / CurrencyUnit.amount.toDouble)        formatted ("%."+ decimals(CurrencyUnit.amount) +"f")        +" "+ designation)   }
  val CurrencyUnitCurrency }
Listing 20.14 - The full code of class CurrencyZone.

You can test the class in the Scala command shell. We'll assume that the CurrencyZone class and all concrete CurrencyZone objects are defined in a package org.stairwaybook.currencies. The first step is to import everything in this package into the command shell:

  scala> import org.stairwaybook.currencies._
You can then do some currency conversions:
  scala> Japan.Yen from US.Dollar * 100
  res16: Japan.Currency = 12110 JPY
  
scala> Europe.Euro from res16 res17: Europe.Currency = 75.95 EUR
scala> US.Dollar from res17 res18: US.Currency = 99.95 USD
The fact that we obtain almost the same amount after three conversions implies that these are some pretty good exchange rates!

You can also add up values of the same currency:

  scala> US.Dollar * 100 + res18
  res19: currencies.US.Currency = 199.95
On the other hand, you cannot add amounts of different currencies:
  scala> US.Dollar + Europe.Euro
  <console>:7: error: type mismatch;
   found   : currencies.Europe.Euro
   required: currencies.US.Currency
         US.Dollar + Europe.Euro
                            ^

By preventing the addition of two values with different units (in this case, currencies), the type abstraction has done its job. It prevents us from performing calculations that are unsound. Failures to convert correctly between different units may seem like trivial bugs, but they have caused many serious systems faults. An example is the crash of the Mars Climate Orbiter spacecraft on September 23, 1999, which was caused because one engineering team used metric units while another used English units. If units had been coded in the same way as currencies are coded in this chapter, this error would have been detected by a simple compilation run. Instead, it caused the crash of the orbiter after a near ten-month voyage.

20.10 Conclusion [link]

Scala offers systematic and very general support for object-oriented abstraction. It enables you to not only abstract over methods, but also over values, variables, and types. This chapter has shown how to take advantage of abstract members. They support a simple yet effective principle for systems structuring: when designing a class, make everything that is not yet known into an abstract member. The type system will then drive the development of your model, just as you saw with the currency case study. It does not matter whether the unknown is a type, method, variable or value. In Scala, all of these can be declared abstract.


Footnotes for Chapter 20:

[1] There's some promising recent research on virtual classes, which would allow this, but virtual classes are not currently supported in Scala.

[2] Scala uses rich wrappers, described in Section 5.9, to make formatted available.

[3] By the way, in case you think you're getting a bad deal on Japanese yen, the exchange rates convert currencies based on their CurrencyZone amounts. Thus, 1.211 is the exchange rate between US cents to Japanese yen.





Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use