This course has already ended.

The latest instance of the course can be found at: O1: 2024

Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.

Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.


Chapter 7.2: Traits

About This Page

Questions Answered: How can I represent supertype–subtype relationships in my program? Concepts that have subconcepts? How can I make my program more generic and easier to extend?

Topics: Using traits for representing supertypes and subtypes. Abstract methods and variables. Static types vs. dynamic types. Sealed traits.

What Will I Do? Read and answer questions. There’s one programming assignment, too.

Rough Estimate of Workload:? Somewhat over an hour, excluding the optional parts.

Points Available: B35.

Related Modules: Subtypes (new).

../_images/person04.png

Introduction: Two-Dimensional Shapes

Suppose we’re writing a program that deal with two-dimensional shapes such as circles and rectangles. Among other things, the program must be able to compute the areas of such shapes.

Note that we’re not dealing with images of circles or rectangles, for which we’ve used the Pic type. Instead, we seek to represent the mathematical concepts of circle and rectangle.

Here’s a first go at defining class Circle:

import scala.math.Pi

class Circle(val radius: Double) {

  def area = Pi * this.radius * this.radius

  // Etc. Other Circle methods go here.

}

And class Rectangle:

class Rectangle(val sideLength: Double, val anotherSideLength: Double) {

  def area = this.sideLength * this.anotherSideLength

  // Etc. Other Rectangle methods go here.

}

There’s nothing wrong with either class if we consider them individually. We could use each one independently in a program, but:

But #1

What if we want to compare different shapes by area?

We could add methods like so:

import scala.math.Pi

class Circle(val radius: Double) {

  def area = Pi * this.radius * this.radius

  def isBiggerThan(another: Circle): Boolean = this.area > another.area

  def isBiggerThan(rectangle: Rectangle): Boolean = this.area > rectangle.area

}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) {

  def area = this.sideLength * this.anotherSideLength

  def isBiggerThan(another: Rectangle): Boolean = this.area > another.area

  def isBiggerThan(circle: Circle): Boolean = this.area > circle.area

}

But must we really write two separate methods? And replicate them in each class? Not very DRY. And what if there were more than two kinds of shapes?

And that’s not all.

But #2

What if we want a collection that stores references to different shape objects: circles, rectangles, and perhaps other shapes, too? Like this:

object ShapeTest extends App {
  val shapes = Buffer[?????]()
  shapes += new Circle(10)
  shapes += new Rectangle(10, 100)
  shapes += new Circle(5)

  var sumOfAreas = 0.0
  for (current <- shapes) {
     sumOfAreas += current.area
  }
  println("The sum of the areas is: " + sumOfAreas)

}
What should we write in place of ?????? What can we specify as the type of the elements in shapes?
On a related note: what is the type of current? It should be something like “any object that has an area method”, right?

Surely we don’t need to make separate lists for circles, rectangles, etc.? That wouldn’t be too practical.

We can shed some light on the problem by initializing the buffer’s elements as we create it:

object ShapeTest extends App {
  val shapes = Buffer(new Circle(10), new Rectangle(10, 100), new Circle(5))

  var sumOfAreas = 0.0
  for (current <- shapes) {
     sumOfAreas += current.area
  }
  println("The sum of the areas is: " + sumOfAreas)

}
The command is perfectly valid. Scala infers the types of expressions automatically where possible (Chapter 1.8). Since the elements include circles and a rectangle, the Scala compiler determines that this isn’t a “buffer of circles” or a “buffer of rectangles” but a “buffer of miscellaneous objects”. (The name of the inferred type is Buffer[AnyRef]. We’ll discuss the meaning of AnyRef further in Chapter 7.3.)
Since the buffer contains miscellaenous objects, current may refer to any such object. Which is why the attempt to call current.area now gives a compile time error to the effect that “The area method does not exist on any old object.”

It’s true that the method doesn’t exist on all objects. It’s great that the compiler questions our method call current.area. If we have a buffer of truly miscellaneous objects, we indeed should not be able call area on the buffer’s contents.

On the other hand, we happen to know that the objects that we put in this buffer do have something in common: the area method. It would be both natural and convenient if our ShapeTest app worked.

What to do?

Subordinate and Superordinate Concepts

This is a circle. →
← This is also a shape.

We humans think of circles and rectangles as shapes. Shapes, in general, have an area. To us, it’s natural that a thing can be an instance of a more specific concept (such as circle) as well as its superordinate concept (such as shape). We may conceive of the properties of our program in these terms:

  • “This program computes the total area of some shapes.”
  • “All shapes have an area method.”
  • isBiggerThan expects any shape object as a parameter.”

This diagram illustrates the relationships:

../_images/inheritance_shape.png

An “is-a relationship” links the subordinate concepts to the more general one: every circle is a shape, for example.

We can also express this notion as code. This chapter describes one way to do that: traits. The next chapter describes another way: superclasses.

Defining a Superordinate Concept as a Trait

Let’s represent the concept of shape as a data type:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double    

}
We define a trait (piirreluokka) named Shape. Its purpose is to provide a generic definition for shapes: “In this program, in order for an object to qualify as a shape, it must possess the following methods — no matter what other properties it has or doesn’t have.”
We use the keyword trait rather than class but, that aside, this is pretty much like a regular class definition.
We define: all shapes have an isBiggerThan method that compares the areas of two shapes.
The parameter has the type Shape, meaning that we can pass in exactly the sort of object that the trait describes. This illustrates that we can use a trait as a data type just like we’ve used regular classes.
We define: all shapes have an area method for computing the size of the shape. But: we define only the method’s name, its parameters (none, in this case), and its return type (Double).
What we don’t define is an actual implementation for computing the area. Our method has no body! Such a method is known as an abstract method (abstrakti metodi).

Unlike the regular classes that we’ve written so far, a trait can contain abstract, unimplemented methods. The Shape trait says any shape’s area can be computed with a parameterless area method that produces a Double. How that Double is produced must be defined separately for different shapes.

Defining a subtype

We have now defined the generic concept of Shape but haven’t yet stated that circles and rectangles are shapes. Here’s how:

import scala.math.Pi

class Circle(val radius: Double) extends Shape {

  def area = Pi * this.radius * this.radius

}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {

  def area = this.sideLength * this.anotherSideLength

}
The keyword extends marks Circle as a subtype of Shape.
You can read the definition as: “The Circle class extends the Shape trait.” Or: “Class Circle mixes in trait Shape.” In any case, the idea is that all objects of type Circle aren’t only Circles but also have the type Shape and all the properties defined in trait Shape.
A class can implement the abstract methods of a trait. For instance, here we define that a circle is a shape whose area comes from the formula π * r2 and a rectangle is a shape whose area is the product of its two sides.

Note: extends Shape implies that circle and rectangle objects now also have an isBiggerThan method even though we didn’t write that method in the two classes.

A trait as a type for objects

Given the above, we now know that:

  • Like regular classes, traits define data types.
  • You can use the name of a trait to annotate a type onto a variable, just like you can use a class name.
  • It’s possible to create objects that are of type Shape.

Does that mean you can write new Shape to create a new Shape object? If you do, what do you get?

Let’s try it:

new Shape<console>:12: error: trait Shape is abstract; cannot be instantiated

No. There are no Shape objects that are “just shapes”, and you can’t use new to create such an object. Which is good, since we haven’t defined an implementation of area for such hypothetical objects.

We must instantiate a trait indirectly by creating instances of classes that extend the trait, as illustrated below. Let’s start with a circle:

val myCircle = new Circle(1)myCircle: o1.shapes.Circle = o1.shapes.Circle@1a1a02e

Next, we’ll try out a previously unfamiliar method named isInstanceOf, which is available on all Scala objects. It tells us whether or not an object is of a given type.

myCircle.isInstanceOf[Circle]res0: Boolean = true
Unlike most of the methods that we’ve used, isInstanceOf takes a type parameter, which goes in square brackets.
We ask the object that myCircle refers to whether it is of type Circle. It is.

Now let’s see if the same object is a Shape:

myCircle.isInstanceOf[Shape]res1: Boolean = true

True again. Our object has multiple types at once, as we wanted.

Of course, our object does not have all possible types. It’s not a Rectangle, for example.

myCircle.isInstanceOf[Rectangle]<console>:13: warning: fruitless type test: a value of type o1.shapes.Circle cannot also be a o1.shapes.Rectangle
              myCircle.isInstanceOf[Rectangle]
                                   ^
res2: Boolean = false

A trait as a type for collection elements

Vector(new Circle(1), new Circle(2))res3: Vector[o1.shapes.Circle] = Vector(o1.shapes.Circle@e17571, o1.shapes.Circle@1e56bea)
Vector(new Circle(1), new Rectangle(2, 3))res4: Vector[o1.shapes.Shape] = Vector(o1.shapes.Circle@876228, o1.shapes.Rectangle@3d619a)
When the collection contains only circles, Scala infers the type as Circle.
When the collection also contains one or more rectangles, Scala finds the shared supertype, namely Shape.

The Introductory Problems, Solved

The problems we faced at the beginning of the chapter simply vanish with the introduction of the Shape trait.

isBiggerThan for all shapes

At the top of this chapter, we wrote multiple isBiggerThan methods on Circle and Rectangle. We don’t need them anymore. Our Shape trait defines a generic isBiggerThan method (replicated below) that works for comparing the areas of circles and rectangles — and any other specific shapes we might wish to add later! The method doesn’t care if the two shapes are instances of the same class or not, as long as they are Shapes of some kind.

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double

}

The two invocations of area in isBiggerThan are valid because of the abstract method a couple of lines below. Despite being abstract, the method guarantees that no matter which specific sort of objects this and another happen to refer to, they will definitely have some sort of implementation for area.

Implementation required!

To make it possible to instantiate Circle and Rectangle, we must implement abstract area method as stipulated in the Shape trait.

If we had left out area from Circle or Rectangle, we would have received a compile-time error about the missing method. That is, the Scala toolkit ensures that any concrete object that we create has actual implementations for any abstract methods in its supertypes. We can count on objects of any type to have implementations for all methods of that type.

ShapeTest also works now

Now that we have the Shape trait, our original test app works unchanged:

object ShapeTest extends App {
  val shapes = Buffer(new Circle(10), new Rectangle(10, 100), new Circle(5))

  var sumOfAreas = 0.0
  for (current <- shapes) {
     sumOfAreas += current.area
  }
  println("The sum of the areas is: " + sumOfAreas)

}
The inferred type of the buffer’s elements is Shape. We could have also written Buffer[Shape](...) here if we had felt like it.
Calling current.area is now legal because current is of type Shape and the trait guarantees that all Shape objects have an implementation for area.

Questions about Variables and Types

Which of the code snippets below make sense to you? Which of them are valid? Which of them are invalid and why?

Assume that trait Shape is defined as above, as are its subtypes Circle and Rectangle.

The answers are relatively commonsensical. If you don’t know, take a guess. The phenomena highlighted by the questions are discussed below.

var test = new Rectangle(10, 10)
println(test.area)
test = new Rectangle(10, 20)
println(test.area)
var test = new Shape(10, 20)
println(test.area)
var test = new Rectangle(5, 10)
println(test.area)
test = new Circle(10)
println(test.area)
var test: Shape = new Circle(10)
println(test.area)
test = new Rectangle(10, 20)
println(test.area)

Static Types vs. Dynamic Types

Let’s recap a couple of terms from Chapter 1.2:

  • We use the word static for the aspect of a program that exists even when the program is not running. That is, the word refers to things that are present in the program text or directly deducible from it.
  • We use the word dynamic for the aspect that a program gains by being run: the process of program execution and things that happen during that process. All the dynamic properties of a program aren’t present in the program code alone; user input can impact on what happens during a program run, for example.

In Chapter 1.3, we noted that expressions have values and those values have types. In Chapter 1.4, we saw that variables have types. That chapter also told us that when you assign a value to a variable, the value’s type must be compatible with the variable’s type. Until now, “must be compatible” has effectively meant “must be the same as”. However, now that we’ve introduced traits, it’ time to consider compatibility a bit more carefully.

In statically typed languages such as Scala, we can distinguish between static types and dynamic types, as discussed below.

Static types

A static type (staattinen tyyppi) is a type that can be determined by examining program code, without running the program, and that is unaffected by runtime factors such as user inputs. Here are a few examples:

  • We annotate each method parameter with its static type.
  • Given the code val text = "llama", it’s clear that the variable text has a static type of String.
  • The expression 1 + 1 can be inferred to have the static type Int given that each of its subexpressions is a literal with the static type Int.

Variables and expressions have static types. That is, it is those parts of program code, not their values, that have static types!

Static types dictate which operations and method calls are valid and which ones aren’t. A method call like myObject.someMethod() produces a compile-time error unless the myObject has a static type that defines a parameterless someMethod.

Dynamic types

The values of variables and expressions have dynamic types (dynaaminen tyyppi). These types are affected by what happens during a program run.

In many cases, a value’s dynamic type will exactly match the static type of the corresponding variable or expression. For instance, the expression 1 + 1 evaluates to the integer value two, whose dynamic type is Int just like the expression’s static type is.

The following program demonstrates the distinction between static and dynamic types:

var test: Shape = new Rectangle(10, 20)
println(test.area)
test = new Circle(10)
println(test.area)
val selected = readLine("Would you like a circle? Say 'yeah' if you do, or you'll get a square. ")
if (selected == "yeah") {
  test = new Circle(readLine("Radius: ").toInt)
} else {
  val side = readLine("Side length: ").toInt
  test = new Rectangle(side, side)
}
println(test.area)
We give test the type Shape so that it can store a reference to any object whose class extends Shape.
Each use of the name test as an expression therefore has the static type Shape. That means it’s legal to call test.area, since the area method is defined on Shape.
However, the value of the expression test has a dynamic type that isn’t Shape. As this program is run, the variable first refers to a Rectangle object, then a Circle. On the last line, the value of test has a dynamic type that depends on which string the user has entered.

Each time we call area, one of the different implementations for that method is activated. Which one, depends on the dynamic type of the value that we call the method on; the names of methods are dynamically bound to implementations. In other words: what happens as a reaction to a message sent to an object depends on the specific type of the object that receives the message.

As you see, a value’s dynamic type doesn’t need to be identical with the static type of the corresponding variable; it needs to be type compatible (tyyppiyhteensopiva). You can store a reference to a Circle object in a variable of type Circle or a variable of type Shape, but not in a variable of type String or Obstacle.

Types in the REPL

Here are a few more examples in the REPL. Observe how the REPL outputs the static types of the variables to the left of the equals sign, as well as the dynamic types on the right as part of the objects’ descriptions.

var test1 = new Rectangle(5, 10)test1: o1.shapes.Rectangle = o1.shapes.Rectangle@38c8ed

The static type of the variable test1 is inferred from the initial value assigned to the variable. Since that static type is Rectangle, we can’t make the variable refer to a circle object. An attempt to do so provokes a clearly worded error message:

test1 = new Circle(10)<console>:11: error: type mismatch;
 found   : o1.shapes.Circle
 required: o1.shapes.Rectangle
       test1 = new Circle(10)
             ^

However, if we specifically give the variable the Shape type, that static type is “wider” than the value’s dynamic type:

var test2: Shape = new Rectangle(5, 10)test2: o1.shapes.Shape = o1.shapes.Rectangle@bdee1c

This variable can also hold a reference to a circle:

test2 = new Circle(10)test2: o1.shapes.Shape = o1.shapes.Circle@1071884

Questions about traits and types

Take another look at ShapeTest:

object ShapeTest extends App {
  val shapes = Buffer(new Circle(10), new Rectangle(10, 100), new Circle(5))

  var sumOfAreas = 0.0
  for (current <- shapes) {
     sumOfAreas += current.area
  }
  println("The sum of the areas is: " + sumOfAreas)

}
What is the static type of the variable current?
In the area below, please list the dynamic type of each value stored in current as the program runs. Enter each type on a separate line, in order. (Just the class names, please; leave out the package name.)

In this question and the ones below it each give you a piece of code. Assess whether that code is valid or if it produces a compile-time error.

var test = new Circle(10)
println(test.radius)
var test: Shape = new Rectangle(10, 20)
println(test.radius)
var test: Shape = new Rectangle(10, 20)
println(test.area)
test = new Circle(10)
println(test.radius)
var test: Shape = new Circle(10)
println(test.radius)

What’s the point of that restriction?

Why does it make sense that the static type of an expression decides which operations are valid? Why should it be illegal to use radius as shown in the last example above?

This section should give you an idea of the answers. If you feel you have a good idea already, it’s fine to skip ahead.

Restricting operations by static type is especially relevant in methods that use Shape or another supertype in their parameter. An example is isBiggerThan in trait Shape:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double

}
The variable another refers to some shape object; the variable’s static type is Shape. The method call another.area is valid only because area is a method on this static type.
In contrast, we couldn’t rewrite this method to compare shapes by their radiuses. The expression another.radius would be invalid since not all the objects that can be passed as parameters to isBiggerThan have a radius. If another.radius was accepted by the compiler, and we then called the method and passed in a rectangle, the method would fail and crash our program at runtime.
The same goes for this, whose static type is Shape because the code is within the Shape trait. this.radius is just as invalid as another.radius.

In more general terms: when you use a supertype in a method parameter, that method can use the parameter only for the things that are shared by all instances of the supertype. This ensures that the method works for any and all instances of all subtypes.

Static types help the programmer’s tools (the compiler, in particular) locate errors and to notify you before you run your program. If the tools didn’t enforce method calls in this way, we’d suffer a great loss in type safety (Cf. what was said about static and dynamic typing at the end of Chapter 1.8.)

Working around that restriction

But what if you do have a reference to a Circle object stored in a Shape variable and really want to access its radius property? Or what if you want to make a decision during a program run based on whether a Shape variable happens to refer to a circle or some other shape?

In such cases, you can turn to match (Chapter 4.3) for assistance. match uses the dynamic type of a value as it selects among alternatives:

var someShape: Shape = new Circle(10)someShape: Shape = Circle@1de2de2
someShape match {
  case someCircle: Circle =>
    println("It's a circle and its radius is " + someCircle.radius)
  case _ =>
    println("It's not a circle!")
}It's a circle and its radius is 10.0
In case someShape happens to hold a reference to a Circle, that reference gets stored in the variable someCircle. The static type of someCircle is Circle.
You can read case _ as “in all other cases” (Chapter 4.4).

An alternative method that is usually worse

Another option is to use the asInstanceOf method, which all Scala objects have. In the example below, notice the type parameter and the static type of the return value.

someShape.asInstanceOf[Circle]res5: Circle = Circle@1de2de2
someShape.radius<console>:14: error: value radius is not a member of Shape
            someShape.radius
                      ^
someShape.asInstanceOf[Circle].radiusres6: Double = 10.0

Be warned! This would have caused a runtime error in case someShape hadn’t happened to refer to a circle. The compiler can’t check the dynamic type for you. By electing to use asInstanceOf, you the programmer bypass the strong typing of the language and weaken your program’s type safety. It’s then entirely up to you to ensure that the object’s dynamic type really matches what you wrote in square brackets after asInstanceOf. Use this method carefully if at all.

asInstanceOf corresponds to what is known in many other programming languages as a type cast or simply a cast (tyyppimuunnos).

Programming Assignment: Using and Editing a Trait

Part 1 of 2: a new kind of shape

Write a new subtype for Shape: class RightTriangle, which represents triangles with a 90-degree angle. Right triangles should have all the general properties that shapes have as well as an additional method hypotenuse. The class should work as shown below.

val triangle = new RightTriangle(3.0, 4.0)triangle: o1.shapes.RightTriangle = o1.shapes.RightTriangle@18bcb2d
triangle.hypotenuseres7: Double = 5.0
triangle.areares8: Double = 6.0
new Circle(3).isBiggerThan(triangle)res9: Boolean = true
triangle.isBiggerThan(new Rectangle(7, 5))res10: Boolean = false

In this assignment, you must not define isBiggerThan within the RightTriangle class, nor is there any reason to do so. Triangles, like circles and rectangles, should have this method by virtue of being Shapes.

Part 2 of 2: perimeters of shapes

  1. Add an abstract method perimeter in trait Shape in the Subtypes module. This parameterless method should return the shape’s perimeter (the total length of the shape’s border) as a Double.
  2. After adding this method in Shape.scala, notice how IntelliJ disapproves: the definitions of the three subtypes are no longer valid because they fail to implement perimeter.
  3. Implement perimeter in your new RightTriangle class and in the existing classes Circle and Rectangle. (The module also comes with a class named Square, which we’ll discuss later.) Use cases:
triangle.perimeterres11: Double = 12.0
new Circle(5).perimeterres12: Double = 31.41592653589793
new Rectangle(2, 5).perimeterres13: Double = 14.0

A+ presents the exercise submission form here.

Optional add-ons

You can try adding a triangle or two to the buffer in ShapeTest. This should be easy, assuming you defined RightTriangle appropriately.

You may also wish to write toString methods in RightTriangle and the other shape classes so that they are easier to work with in the REPL. Or you could write a generic toString for all shapes in trait Shape, if you prefer.

Thoughts about traits

Traits are handy. What’s more, defining supertypes can greatly improve program quality. For example:

  • With traits, you can create program components that are more generic. You can, for instance, define a method such as isBiggerThan, which works equally well on shapes of different types.
  • Traits reduce redundancy, which makes your code more maintainable. They can also make your programs easier to extend by adding new subtypes. For instance, when you added triangles to our program, you just wrote extends Shape and implemented area. That was enough to give triangle objects isBetterThan, too, and to make triangles usable in any part of the program that calls for a Shape.

Practice on Traits and Abstract Methods

Consider the example code below.

trait T1 {
  def m1 = 1
  def m2: Int
}
class A(val x: Int, val y: Int) extends T1 {
  def m2 = this.x + 1
  def m3 = this.y + 1
}
class B(val x: Double, val y: Int) extends T1 {
  def m2 = this.x.toInt + 1
  def m3 = this.y + 2
}

Which of the following claims are correct? Select all that apply.

Assume that T1, A, and B have been defined as above and that there may also be other classes in the program that extend trait T1.

Multiple Supertypes

Supertypes at different levels

../_images/inheritance_person.png

You can extend a trait with another trait. Here is an example:

trait PersonAtAalto {
  // Here, we can add methods and/or instance variables that are common to
  // all people at Aalto, whether they are employees, students, or visitors.
}
trait Employee extends PersonAtAalto {
  // Here we can add methods and/or instance variables for Aalto employees.
  // All employees have these properties in addition to the generic ones
  // they get from the PersonAtAalto trait.
}

Now we can define class TeachingAssistant so that it has the Employee trait, thus turning assistants into employees and persons:

class TeachingAssistant extends Employee {

  // Assistant objects have all the methods and instance variables defined
  // in the Employee trait as well as those defined in PersonAtAalto.

  // Here, we can add variables and methods that are specific to teaching
  // assistants.

}
../_images/inheritance_multiple.png

Multiple immediate supertypes

In the previous example, the TeachingAssistant class had an immediate supertype in Employee and an additional indirect supertype in PersonAtAalto. You can also give a class (or trait) multiple direct supertypes, as shown below.

Suppose we have an additional trait, Student. We wish to model the fact that course assistants are students in addition to being employees. This is easy:

class CourseAssistant extends Employee with Student {
  // Now assistants are of all the following types: CourseAssistant, Student,
  // PersonAtAalto, Employee. (They gain the properties of PersonAtAalto just
  // once even though there are two "paths" to that trait.)
}

If there are still more traits you wish to mix in, you can use with repeatedly:

class X extends MyTrait1 with MyTrait2 with MyTrait3 with MyTrait4 with Etc

The traits don’t need to have a shared supertype (like PersonAtAalto is for Employee and Student in our example). You can mix in multiple traits that are otherwise unrelated to each other.

Here is the code from the previous exercise:

trait T1 {
  def m1 = 1
  def m2: Int
}
class A(val x: Int, val y: Int) extends T1 {
  def m2 = this.x + 1
  def m3 = this.y + 1
}
class B(val x: Double, val y: Int) extends T1 {
  def m2 = this.x.toInt + 1
  def m3 = this.y + 2
}

Let’s add a couple of definitions:

trait T2 {
  def m4 = 4
}
class C extends T1 with T2 {
  def m2 = 2
  def m3 = 3.0
}

Which of these claims are correct? Select zero, one, or two items as appropriate.

Tools in package o1 (and FlappyBug, once more)

The course library o1 contains a HasVelocity trait, which is a generic type for objects that have a location and a velocity in two-dimensional space. We have already modeled some such objects in our programs, albeit without using this trait. Let’s explore how the trait can help us.

Take a look at the Scaladocs for HasVelocity. It extends another trait, HasPos; take a look at that, too. If you didn’t do the optional assignment in Chapter 3.6 that introduced class Velocity, start by reading about that class.

Consider how you could add the HasVelocity trait to the classes Bug and Obstacle in FlappyBug. Consider the implications of doing so. Do so. See below for hints if you want them.

Hints for Obstacle:

  • You’ll need an extends at the top, of course.
  • The class needs to implement the abstract method velocity as specified in the HasVelocity trait. Add the method. An obstacle’s speed is constant along the x axis and zero along the y axis.
  • HasVelocity provides a nextPos method. Use it to
    (re)implement approach.

Hints for Bug:

  • You’ll need another extends and another velocity method.
  • You can again use nextPos to simplify the code that moves the bug. (You’ll probably either edit fall or the helper method move if you wrote it in an earlier chapter; the latter becomes rather unnecessary with nextPos, though.)
    • Perhaps you’ll also come up with a way to simplify touches in class Obstacle a bit?

A+ presents the exercise submission form here.

Instance Variables in a Trait

A trait can define instance variables just like a class can.

trait Supertype {
  val magicNumber = 42
  val text: String
}defined trait Supertype
class Subtype extends Supertype {
  val text = "value of 'text' for all Subtype instances"
}defined class Subtype
Like a method, an instance variable on a trait can be abstract.

All instances of extending classes are guaranteed to have the trait’s variables:

val myObject: Supertype = new SubtypemyObject: Supertype = Subtype@714aadf7
myObject.magicNumberres14: Int = 42
myObject.textres15: String = value of 'text' for all Subtype instances

Traits vs. Java’s interfaces

The word interface has many meanings, including these two interrelated ones:

  1. An interface is the “façade” of a program component, such as a class. We use a component via its interface (Chapter 3.2).

  2. In some programming languages (most prominently in Java), there is a specific construct known as an interface, which resembles Scala’s concept of a trait:

    // This is Java, not Scala.
    
    interface Shape { /* etc. */ }
    
    class Circle implements Shape { /* etc. */ }
    

The primary differences between Java’s interface construct and Scala’s traits is that a trait may contain instance variables whereas an interface can’t.

Predefined Instances of Traits

Extending a trait with a singleton

You can use extends to mix in a trait to a singleton object just like you can mix in a trait to a class.

In fact, you’ve already done that. Scala’s App is not a regular class but a trait that represents the concept of a runnable application and offers certain services for starting a program run. When you write extends App on a singleton object, you mix in the App trait to that object. In other words: you make that singleton object a special case of the App supertype.

The example program below also features singleton objects that extend a trait. Moreover, the program introduces the idea that all the possible objects of a particular type may be known in advance and recorded in program code, deliberately preventing other objects of that type from being created.

Blood types revisited

In Chapter 5.1, we modeled people’s blood types as objects. We used a combination of the ABO classification system and the Rhesus classification system as we represented each person’s blood as a combination of a string and a Boolean.

val myBlood = new BloodType("AB", true)myBlood: o1.blood.BloodType = AB+
val yourBlood = new BloodType("A", true)yourBlood: o1.blood.BloodType = A+
myBlood.canDonateTo(yourBlood)res16: Boolean = false

In that earlier version of the program:

We had only a single BloodType concept: the combination of a person’s ABO type and their Rhesus type.

However, in the real world, it’s possible to use the ABO and Rhesus types independently of each other; what’s more, there are many other blood systems beyond these two. Also:

The user of our class passed in parameters that specify the blood type. We trusted the user to always pass in valid strings.

Let’s rework our program. We’ll make it more versatile and represent the ABO and Rhesus classification systems separately. As we do so, we model each system as a trait.

Consider the Rhesus system first. We can model blood types in this system with a Rhesus trait:

trait Rhesus {
  val isPositive: Boolean
  def isNegative = !this.isPositive
  def canDonateTo(recipient: Rhesus) = this.isNegative || this == recipient
  def canReceiveFrom(donor: Rhesus)  = donor.canDonateTo(this)
}
The class has an abstract variable isPositive. Any object that represents a blood type in the Rhesus system must have this variable and a value for it.
The Rhesus type defines the properties that are common to all blood types in this system: each of the two blood types of the Rhesus system will have these properties.

Since there is a small number of blood types (two), and since we know those types in advance, it’s natural to represent each individual blood type with a singleton object.

Let’s represent the blood types of this system with two singleton objects: the object RhPlus stands for Rhesus-positive blood and RhMinus for Rhesus-negative blood.

object RhPlus extends Rhesus {
  val isPositive = true
  override def toString = "+"
}

object RhMinus extends Rhesus {
  val isPositive = false
  override def toString = "-"
}
Each of the singleton objects extends the Rhesus trait, making them specific cases of the supertype Rhesus. These two objects have all the methods from the trait.
The objects implement the abstract variable from the trait by assigning different Boolean values to it.

Now we can use the objects to work with Rhesus-system blood types:

RhPlus.canDonateTo(RhMinus)res17: Boolean = false
RhMinus.canDonateTo(RhPlus)res18: Boolean = true
RhMinus.canDonateTo(RhMinus)res19: Boolean = true
RhMinus.isPositiveres20: Boolean = false

Speaking more generally: what we did here is represent a concept (the trait) that has a limited, known set of named instances (the singletons) that collectively cover all the possible occurrences of the concept.

Similarly, we could represent the ABO system with an ABO trait and four singleton objects A, B, AB, and O. If we wanted to represent other blood systems, too, we could follow the same pattern.

In the optional assignment below, you get to implement the ABO classification and explore how to combine blood type systems that have been implemented in this manner.

An ABO trait

The Subtypes module contains o1.blood. The file BloodType.scala within that package already contains Rhesus as defined above. In the same file, add the trait ABO and the singletons A, B, AB, and O; those four will be the only objects with that trait.

The ABO trait should have:

  • an abstract antigens variable that stores the particular blood type’s antigens as a string such as "A" or "AB"; and
  • the methods canDonateTo and canReceiveFrom, which work like the methods of the same name from Chapter 5.1 and the Rhesus trait above. However, the new methods should consider only the ABO antigens and disregard the Rhesus factor entirely.

The singleton objects A, B, AB, and O should have:

  • a concrete string value for antigens that lists all the antigens present in the blood type: "A", "B", "AB", and "" (the last being the empty string); and
  • a toString method that returns the blood type’s name. The name equals antigens, except that name of the O type is "O".

If you have trouble remembering which blood types are compatible with which other ones, consult Chapter 5.1, your own solution to the earlier assignment, or that assignment’s example solution.

BloodTest.scala provides a little app object for testing your code. Some of the given code has been commented out to suppress untimely error messages; uncomment it when you’re ready to test.

A+ presents the exercise submission form here.

What about combining the blood systems?

Our goal was to be able to use just one of the blood type systems separately, or both in combination, or even to combine these systems with others. How can we combine the systems now that we represent each one with a trait of its own?

One approach is implemented below as class ABORh, which you can also find in BloodType.scala.

class ABORh(val abo: ABO, val rhesus: Rhesus) {

  def canDonateTo(recipient: ABORh) =
    this.abo.canDonateTo(recipient.abo) && this.rhesus.canDonateTo(recipient.rhesus)

  def canReceiveFrom(donor: ABORh) = donor.canDonateTo(this)

  override def toString = this.abo.toString + this.rhesus.toString

}
The class provides essentially the same functionality as our earlier BloodType class. This class has been built differently, though, as a combination of the ABO and Rhesus systems.
This class doesn’t count on the user to pass in valid strings. It accepts only values of type ABO and Rhesus; in practice, we pass in the singleton objects with those traits.

Sealing a trait

The revised blood type program is based on the idea that there are only two specific objects that extend the Rhesus trait and four specific objects that extend ABO. Those objects, which we detail in the same file as the traits, constitute all the possible instances of the traits; any user of the Rhesus and ABO types should be able to rely on the fact that no surprising instances of those types pop up for any reason. We mean for the traits Rhesus and ABO to be “sealed”: we have already defined everything that extends them, and no user of the traits should define more.

We can express that thought as Scala:

sealed trait Rhesus {
  // ...
}
The word sealed at the top of a trait means that any class or object that directly extends the trait must be defined in the same file as the trait.

Go ahead and add sealed to Rhesus (and ABO) in your code.

(In this example, we sealed a trait. It’s also possible to seal a regular class, which is a topic for Chapter 7.3.)

On enumerated types

As you saw, the combination of sealed trait and singleton objects is a nice way to represent a type with a known, comprehensive set of possible values. Many programming languages use so-called enumerated types for the same purpose. Scala, too, supports enumerated types, but the trait-based approach described above is often preferable.

(The newly released Scala 3 has improved support for enumerations.)

A deeper dip into Scala’s type system

(This completely optional assignment is harder than the previous one and calls for independent study from resources beyond this ebook. At this point, this assignment is best suited to only those students who have prior experience from before O1. Beginners should probably skip this for now.)

We already have representations for the Rhesus system, the ABO system, and their combination ABORh. Those three types have a few things in common: they have a canDonateTo method, they have a canReceiveFrom method (identically implemented in all three), and they each represent a particular blood-type system.

Let’s define a supertype for the three types and other any other blood-type systems we might wish to define in the future. Rhesus, ABO, and ABORh are all BloodTypes.

Here’s a first attempt:

trait BloodType {
  def canDonateTo(recipient: BloodType): Boolean
}

sealed trait Rhesus extends BloodType { /* etc. */  }
sealed trait ABO    extends BloodType { /* etc. */  }
sealed trait ABORh  extends BloodType { /* etc. */  }
The type annotation on the parameter causes a problem that manifests itself in the subtypes Rhesus, ABO, and ABORh.

Find out what error message you get from the above program. Reflect on why that happens.

Then look at this version:

trait BloodType[ThisSystem] {
  def canDonateTo(recipient: ThisSystem): Boolean
}
We’ve added a type parameter on the trait.

Read about type parameters in Scala’s documentation and other resources. Determine what we need to add to the subtypes Rhesus, ABO, and ABORh to make them compatible with the new version of BloodType? Make those changes in your copy of this code. (There’s no automatic feedback available for this assignment. You’ll need to test and assess your solution on your own.)

That trait had only the canDonateTo method. In the next version, we’d like to add canReceiveFrom and remove its identical implementations from the three subtypes:

trait BloodType[ThisSystem] {
  def canDonateTo(recipient: ThisSystem): Boolean
  def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
}

That change broke our trait, however. Find out what the error is and see if it makes sense to you.

In this final version, both method definitions are fine:

trait BloodType[ThisSystem <: BloodType[ThisSystem]] {
  this: ThisSystem =>
  def canDonateTo(recipient: ThisSystem): Boolean
  def canReceiveFrom(donor: ThisSystem) = donor.canDonateTo(this)
}

What’s that? In brief, it is an upper bound and specifies that the type parameter ThisSystem must be some subtype of BloodType[ThisSystem].

And what’s that? In a nutshell, it is a self type and specifies that, in this trait, this refers to an object of type ThisSystem.

Find out how these changes solve the problems in the previous version. Search online for more information as needed.

Summary of Key Points

  • A trait is a programming construct similar to a class. It, too, defines a data type, and it, too, is useful for modeling concepts in the program’s domain.
  • You can use traits to represent a superordinate domain concept: a trait can define instance variables and methods that are common to multiple classes, singletons, or traits.
  • A class or singleton object may mix in a trait (or several), giving it the properties defined in that trait and making the trait its supertype.
  • A trait can define abstract methods. An abstract method doesn’t have a generic implementation (method body) in the trait itself; the trait’s subtypes define separate implementations for the method instead. The presence of an abstract method on a trait guarantees that all actual objects with that trait have some implementation for the method. Abstract variables can be defined similarly.
  • If your program features a superordinate concept whose specific objects are known in advance, you can represent those instances as singleton objects that extend the same type.
  • Links to the glossary: trait, abstract method, abstract variable; static type, dynamic type; DRY; abstraction; sealed trait.

Feedback

Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.

Credits

Thousands of students have given feedback that has contributed to this ebook’s design. Thank you!

The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using O1Library for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.

The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It was created by Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi, and Paweł Stróżański with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits appear at the ends of some chapters.

a drop of ink
Posting submission...