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.5: Superclasses and Subclasses

../_images/person04.png

Introduction

In the preceding chapters, we used traits to define supertypes for multiple classes. This chapter complements the preceding ones: we’ll see how a regular class may also serve as a supertype, with other classes inheriting from it.

Let’s Continue with Shapes

In the previous chapter, we defined the trait Shape and an inheriting Rectangle class:

trait Shape:
  def isBiggerThan(another: Shape) = this.area > another.area
  def area: Double
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength

Suppose we also want to represent squares — that is, rectangles with four sides of equal length? We’d like to be able to write Square(10) and the like.

Of course, we might simply write a Square class that inherits Shape:

class Square(val sideLength: Double) extends Shape:
  def area = this.sideLength * this.sideLength

The bothersome thing about this solution is that the code is quite redundant: the algorithm for computing the area of a square is the same as that for a rectangle; it just happens that the sides are equal in length. The code isn’t up to par in terms of conceptual modeling, either: it makes squares a subtype of Shape alongside rectangles, whereas we humans prefer to think of a squares as a special case of a rectangle; each square is a square, a shape, and a rectangle.

The fix is simple: we can define squares as a subtype of rectangles, as shown in the diagram below. We can do this even though Rectangle is a regular class, not a trait.

../_images/inheritance_shape_square.png

Subclasses and Superclasses

Let’s add something to class Rectangle:

open class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength

This is a regular class, which can be instantiated directly, but it is open (avoin) for extension. By adding the keyword open, we indicate that this class is designed to be freely usable as a superclass (yliluokka): other classes may inherit from it.

Square can then be defined as Rectangle’s subclass (aliluokka):

class Square(size: Double) extends Rectangle(size, size)

We use the familiar extends keyword, but this time we follow it with the name of a regular class rather than a trait. The subclass Square inherits its superclass, and so all Square objects now have the additional type of Rectangle.

Square has only one constructor parameter that determines the length of each side.

To create an instance of a subclass, any initialization steps that the superclass demands must also be performed. A subclass may pass constructor parameters to its superclass; the idea is the same as with the traits of Chapter 7.3. In this example, we state that whenever a Square object is created, we initialize a Rectangle so that each of its two constructor parameters (each side length) gets the value of the new Square’s single constructor parameter.

How mandatory is the open keyword?

There are reasons why it’s not a good idea to write subclasses for any random class that isn’t designed to be inherited from. Those reasons tend to come to the fore in larger-scale programming projects and won’t be obvious from O1’s introductory-level materials. However, the optional material at the end of this chapter hints at some of the complications in class-based inheritance.

Anyhow, we explicitly write open to state that a Scala class has been designed for inheritance and programmers are free to inherit from it as they please. If you omit open, any subtypes that your class might have should be defined in the same file. Technically, omitting open does not utterly prevent inheriting from the class in other files, but the Scala compiler will issue warnings about the suspicious code, and it’s best to avoid such inheritance. (Cf. sealing a class with sealed, which completely prevents direct subtypes from being defined in other files; Chapter 7.4.)

In some exceptional circumstances, it is reasonable to write a subclass for a non-open class that is defined elsewhere. In that case, the programmer may disable the compiler warning with import scala.language.adhocExtensions. In O1, you should have no reason to do this.

Inheritance vs. traits

Inheriting from a superclass looks much like inheriting from a trait. The two techniques are indeed closely related.

There are some differences, though; let’s compare. First, consider traits and how the Rectangle class extends the Shape trait:

When using a trait

Example

The supertype is represented by a trait.

trait Shape

Each subtype inherits that trait.

class Rectangle extends Shape

A trait may define abstract methods and variables.

def area: Double

A trait cannot be instantiated directly.

One does not simply write Shape().

A class (or trait) may directly inherit multiple traits.

class X extends Trait1, Trait2, Trait3 is fine.

A trait cannot pass constructor parameters to its supertype(s).

trait Trait1 extends Trait2(someParams) is invalid.

And here is a similar table about inheritance and the example of Rectangle as a superclass for Square:

When inheriting from a regular class

Example

The supertype is represented by a regular class (which is best to mark as open, if meant for inheritance).

open class Rectangle

Each subtype inherits that superclass.

class Square extends Rectangle

An ordinary class cannot define abstract methods or variables. (But some classes can. See below.)

All the methods on Rectangle have a method body.

A superclass can be instantiated directly.

Rectangle(...) works.

(In Scala and many other languages:) A class cannot have more than one immediate superclass.

class X extends Super1, Super2 is invalid.
(But class X extends Super1, Trait1, Trait2 is fine.)

Any class may pass constructor parameters to its supertype(s).

class Square(size: Int) extends Rectangle(size, size) is fine.

There are many programs where it’s reasonable to use either of the above.

Assignment: Items within Items

Introduction

Imagine we’re working on an application that needs to represent items of various sorts. We have a simple class Item:

class Item(val name: String):
  override def toString = this.name

For this small programming assigment, let’s adopt the following goal: we wish our program to have not only “simple items”, as defined above, but also items that can contain other items. For instance, we might have a bag that contains a book and a box, and the box might further contain a ring.

Task description

Write a subclass for Item. Name it Container. This subclass will represent items that can contain other items, such as bags and boxes:

  • Containers each have a name, just like other items do.

  • They have an addContent method, which places an item within the container.

  • And a contents method that returns the previously added items in a Vector.

  • Their toString implementation differs from that of other items.

Your class should work as shown below.

val container1 = Container("box")container1: o1.items.Container = box containing 0 item(s)
container1.addContent(Item("ring"))
container1res0: o1.items.Container = box containing 1 item(s)
val container2 = Container("bag")container2: o1.items.Container = bag containing 0 item(s)
container2.addContent(Item("book"))
container2.addContent(container1)
container2res1: o1.items.Container = bag containing 2 item(s)
container1.contentsres2: Vector[o1.items.Item] = Vector(ring)
container2.contentsres3: Vector[o1.items.Item] = Vector(book, box containing 1 item(s))

The toString method’s return value should count only the container’s immediate contents. For instance, in the REPL session above, the bag is shown as containing only two items, even though the box in the bag further contains the ring.

(How could we count all the “contents of the contents”, too? You’ll see in Chapter 12.2.)

Instructions and hints

  • In o1.items within the Traits module, you’ll find that Item class. It’s missing the open keyword, though; type that in.

  • In the same package, you’ll find some starter code for Container.

  • Don’t forget to pass a constructor parameter to the superclass Item.

  • Don’t use val to (re)define the instance variable name within the Container subclass. That variable is already defined in the superclass. It’s perfectly fine to give Container’s constructor parameter the name name, though.

  • Note that what you override in this assignment isn’t the generic toString from AnyRef, as in many other programs, but the toString implementation in Item.

  • Can you implement the toString in Container so that you use the superclass’s implementation rather than directly accessing the container’s name? (This is optional.)

  • Do not add any public members in the class beyond what was requested. Private members you are free to add as you see fit.

  • You are free to decide which type of collection (vector? buffer?) to use in Container internally to store the contents. In any case, the contents method must return a Vector. If you make use of a buffer, add import scala.collection.mutable.Buffer at the top of the file.

A+ presents the exercise submission form here.

A Few Words about Abstract Classes

In addition to regular classes and traits, one may define abstract classes ( abstrakti luokka). Abstract classes resemble traits in several ways but aren’t quite the same thing.

For our purposes in O1, abstract classes aren’t a central topic; we’ll generally prefer traits for our subtyping needs. As your programming experience grows, you’ll learn to choose between traits and abstract classes. For now, it is enough to be aware that abstract classes exist; that much is good to know because they aren’t a rarity in Scala programs or in programs written in other languages. Not all programming languages have traits and abstract classes as separate concepts.

Let’s see a quick example. In a Chapter 7.3 program, we had this trait named Entity:

trait Entity(val name: String):
  def contact: NaturalPerson
  def kind: String
  override def toString = s"$name ($kind)"

Two of the methods are abstract. Such methods, whose implementation is left to subtypes to take care of, cannot appear in completely regular classes. However, the following alternative definition of Entity is valid.

abstract class Entity(val name: String):
  def contact: NaturalPerson
  def kind: String
  override def toString = s"$name ($kind)"

The word abstract turns this into an abstract class. Such a class may contain abstract methods just like a trait can. And just like a trait cannot be directly instantiated, neither can an abstract class. Abstract classes are open for extension by default, so there is no need to write open.

A class that is not abstract may be referred to as a concrete class.

An abstract class is similar to a trait in some ways and similar to a concrete superclass in other ways. Let’s compare:

Trait

Abstract
superclass
Concrete
superclass

Can it have abstract methods?

Yes.

Yes.

No.

Can it be directly instantiated?

No.

No.

Yes.

Can it pass constructor parameters to its supertype(s)?

No.

Yes.

Yes.

Can you inherit several of them (listed after extends)?

Yes.

No.

No.

Generally speaking, should I use a trait or an abstract class?

Rule of thumb: Unless you have a specific reason to use an abstract class, use a trait instead, since traits can be extended more flexibly.

It can be tricky to choose between a trait and an abstract class. However, in O1, such decisions are generally made for you and provided in the assignment specifications. You can worry about learning to make these decisions yourself later, in Programming Studio 2, for example. For some more information, you could also take a look Scala Cookbook or Programming in Scala, which are some of the recommendations on our Books and Other Resources page.

Class Hierarchies in the Scala API

Like traits, class-based inheritance is useful in forming type hierarchies.

Some examples of hierarchies can be found in Scala’s standard API. Let’s consider a few.

A hierarchy of GUI elements

The package scala.swing provides a selection of classes that represent GUI elements, building blocks for graphical user interfaces. The diagram below depicts a part of this hierarchy.

../_images/inheritance_swing.png

Some of these classes from package scala.swing are traits, some are regular classes. Each supertype defines generic properties common to all the extending types. At the top level, UIElement defines properties common to all kinds of GUI elements, such as background color and size.

About the Swing GUI library

Chapter 12.4 contains an introduction to the GUI library known as Swing. That optional chapter is at the rear end of O1 and this ebook, but if you’re itching to read more about GUIs, go ahead and read it. Now that you’ve been introduced to inheritance, you should have the sufficient prerequisite knowledge for the chapter.

The Option hierarchy (and sealed superclasses)

../_images/inheritance_option.png

There’s a small class hierarchy associated with the Option type, too.

Since Chapter 4.3, you’ve known that there are two kinds of Option objects. Any object of type Option is either a Some object that contains a value or None. This is actually an example of inheritance: Option is an abstract class that the concrete class Some and the singleton object None extend.

Chapter 7.4 mentioned how to seal a trait: the sealed keyword makes it impossible to directly extend a trait except within the file that defines the trait itself. A superclass can also be sealed, and Option is just such a sealed class.

As we noted in Chapter 4.3, an Option can be either a Some or None (which are defined in the same file) but it can never, ever be anything else. Which is exactly what you want when you use Option. You can’t extend Option with a class of your own, and that’s good.

The mother of all classes: Any

Let’s examine some objects in the Scala REPL:

val miscellany = Vector[Any](123, "llama", true, Vector(123, 456), Square(10), IArray(1, 2, 3))miscellany: Vector[Any] = Vector(123, llama, true, Vector(123, 456), o1.shapes.Square@114c3c7, Array(1, 2, 3))

We create a vector that contains some completely disparate objects: an Int, a String, a Boolean, a Vector of Int, a Square, and an immutable array (a sort of collection).

The type of our vector is Any. That is, what we have is a “vector of any sorts of objects”. This suggests that integers, vectors, squares, and what have you, are all of type Any.

All Scala classes and singleton objects — including those you write — automatically inherit from a class called Any, even though we don’t usually mark this inheritance in code. All the objects in a Scala program always have the Any type in addition to any other types they may have.

It’s possible to use this top-level class as a type for a variable, as illustrated below.

var someObject: Any = "kumquat"someObject: Any = kumquat
someObject.isInstanceOf[Any]res4: Boolean = true
someObject.isInstanceOf[String]res5: Boolean = true
someObject.isInstanceOf[Square]res6: Boolean = false
someObject = Square(10)someObject: Any = o1.shapes.Square@ecfb83
someObject.isInstanceOf[Any]res7: Boolean = true
someObject.isInstanceOf[String]res8: Boolean = false
someObject.area-- Error: ... value area is not a member of Any

As the name suggests, you can use a variable of type Any to refer to any object. The object can be a string or a square, for instance.

So, the static type of someObject is Any, and the expression someObject.isInstanceOf is valid because (and only because) the isInstanceOf method is defined in the Any class and is therefore available on any Scala object.

The attempted method call someObject.area fails even though the variable happens to store a Square object which has an area method. The variable’s static type limits what you can do with it.

In most cases, it’s not sensible to use variables of type Any, since the type’s genericity so constricts what you can do with such variables. With Any as the static type, you can use the variable only for the operations that are defined on Any. Those extremely generic methods that are common to all Scala objects include isInstanceOf, toString, ==, !=, and a handful of others. Generally, it makes sense to use variables with more specific types, which is of course what we’ve been doing already. (See the optional material below for a further discussion.)

The mother of most classes: AnyRef

Scala’s root type Any divides in two main “branches”. It has two immediate subclasses, AnyVal and AnyRef:

../_images/inheritance_any.png

This division to AnyRef and AnyVal has to do with the way the Scala language is implemented and isn’t too significant to the Scala beginner or for learning the basics of programming more generally. Nevertheless, it’s good to be aware that these types exist, so you know why they sometimes crop up in Scaladocs, REPL outputs, and error messages.

AnyVal is a superclass for some API classes that define certain data types that are simple but operate more efficiently than other classes do. The familiar data types Int, Double, Boolean, Char, and Unit descend from AnyVal, as do a few others. It’s relatively uncommon to extend AnyVal in an application; in O1, we don’t do that at all.

AnyRef, on the other hand, is a superclass for all other classes and singleton objects. The classes String and Vector, for instance, derive from AnyRef, as does the Item class we just wrote.

We can try AnyVal and AnyRef in the REPL, too:

val miscellany2 = Vector[AnyVal](123, true)miscellany2: Vector[AnyVal] = Vector(123, true)
val miscellany3 = Vector[AnyRef]("llama", Vector(123, 456), Square(10))miscellany3: Vector[AnyRef] = Vector(llama, Vector(123, 456), o1.shapes.Square@667113)

Int and Boolean descend from AnyVal.

String, Vector, and Square descend from AnyRef.

One more: Matchable

In addition to the aforementioned very generic supertypes at the top of Scala’s hierarchy, you may run into one more: the Matchable trait is a supertype for all Scala types on which pattern matching (match) works. Both AnyRef and AnyVal inherit Matchable, so Matchable is nearly as all-encompassing as Any.

Pattern matching works on almost all types in Scala, but there are some very exceptional types that don’t have the Matchable trait. For more information, you could see online, but for course purposes, there’s no need to.)

A bit more on AnyRef and AnyVal

Some readers (who already know something about the JVM/Java) may wish to know that in the JVM-based Scala implementation, subclasses of AnyVal have been implemented as JVM primitives such as int and double, whereas classes that derive from AnyRef are implemented as classes within the JVM.

The names AnyRef and AnyVal reflect this implementation. The former’s implementation relies on object references, the latter’s implementation on primitive values. AnyVals must be immutable and conform to other strict conditions as well. When used in the right places, AnyVals can improve efficiency.

Tailoring Instances of a Supertype

In Chapter 2.4, you learned to create an object that has an instance-specific method in addition to the methods defined in a class:

object superman extends Person("Clark"):
  def lenna = "WOOSH!"
end superman// defined object superman

Since then, we’ve used this technique in combination with Views: we have defined singleton objects that extend View and have custom methods.

Tailoring instances in this way is actually a form of inheritance. For example, what the above command actually does is inherit a single object from the Person superclass. Along the same lines, we have written diverse implementations for the abstract makePic method of the supertype View.

When creating a single object, you can combine superclasses and traits flexibly in various other ways as well. Read on if you wish.

Defining a subtype “on the fly”

You can also inherit a trait “on the fly” as you create an object.

To set up the next example, let’s define a couple of traits and one concrete class. These three types are entirely distinct from each other.

class Animal(val species: String):
  override def toString = "an animal, more specifically a " + this.species
trait Spitting:
  def spit = "ptui!"
trait Humped:
  def numberOfHumps: Int// defined class Animal
// defined trait Spitting
// defined trait Humped

Let’s try a new way of creating an object:

val pet = new Animal("llama") with Spittingpet: Animal & Spitting = an animal, more specifically a llama

We define, “on the fly”, a new subclass of Animal that has no specified name and that also inherits the Spitting trait. We immediately instantiate this new type to produce a single instance of it.

The word new is required in command such as this, which not only create a new instance but also define a new type for it. Note the use of with as well.

This object is of the combined type Animal & Spitting and has all the properties of Animals as well as all the properties defined in the Spitting trait:

pet.speciesres9: String = llama
pet.spitres10: String = ptui!

Of course, we could have also defined a named class, as shown below, and instantiated it as per usual.

class Llama extends Animal("llama"), Spitting// defined class Llama

Adding methods on a type defined “on the fly”

Let’s continue our toy example. We’ll create an object of a type that:

  • is a subclass of Animal;

  • additionally inherits the traits Humped and Spitting; and

  • implements the abstract method numberOfHumps in the Humped trait in a specific way.

val shipOfTheDesert = new Animal("dromedary") with Humped with Spitting:
  def numberOfHumps = 1shipOfTheDesert: Animal & Humped & Spitting = an animal, more specifically a dromedary

Finally, here’s how you could use the same technique to produce an object of type Spitting with its own particular implementation of toString:

val fisherman = new Spitting:
  val name = "Eemeli"
  override def toString = this.namefisherman: Spitting = Eemeli

Here we followed new with the name of a trait rather than a concrete class. However, this command does not directly instantiate the trait (which isn’t even possible). Instead, it defines a new anonymous type that inherits the trait and additionally has the properties listed below. It is that new type that is then instantiated.

Terms for googling: scala trait mixin, scala anonymous subclass.

Further Reading

Choosing static types for variables

Rule of thumb: select the static types of your variables so that they are “wide”. This applies to parameter variables especially. That is, where possible, we’d like our parameters to have a superclass or a trait as their type rather than a more specific class. That way, our methods and classes will be more generally useful and easier to modify.

Compare:

def doSomething(circle: Circle) =
  // ...

vs.

def doSomething(shape: Shape) =
  // ...

The first definition is sensible in case doSomething should work only on circles (say, because it depends on being able to access the given object’s radius, which other shapes don’t have). Otherwise, the second definition is probably better, since it works on different shapes, including future subclasses of Shape that haven’t even been written yet.

You can’t always generalize; otherwise we’d just give everything the Any type. But do generalize when you can.

Between public and private: protected

A subclass’s instances also have the superclasses’ private variables as part of their state. Inherited methods commonly use those private variables. However, you can’t directly refer to a private member of a superclass from the subclass’s program code. The same goes for private members on traits.

However, Scala, like some other languages, features another access modifier, protected. This modifier allows precisely what we just said private disallows: a protected variable or method is accessible not only to the class or trait itself but also within the code of its subtypes. (But not just anywhere, like a public member is.) An internet search should bring up many examples.

Multiple inheritance

As noted above, a class can extend only a single superclass directly. Why?

Search online for multiple inheritance (moniperintä). You may also wish to look up the “deadly diamond of death” that is sometimes associated with multiple inheritance and that is held to be a serious problem by some but not all programmers.

And here is some slightly provocative but interesting reading (mostly to those students who already know something about Java and its interface construct): 'Interface' Considered Harmful.

If my class inherits multiple traits, which method implementations do I get?

Here’s a simple example:

trait X:
  def method: String
trait A extends X:
  override def method = "a’s method"
trait B extends X:
  override def method = "b’s method"// defined trait X
// defined trait A
// defined trait B
class AB extends A, B
class BA extends B, A// defined class AB
// defined class BA
AB().methodres11: String = b’s method
BA().methodres12: String = a’s method

For more information, look up scala linearization or see Books and Other Resources page for further reading.

The Liskov substitution principle

../_images/liskov-en.png

“Square as a subclass of rectangle” is a classic example when discussing the Liskov substitution principle, which we’ll only paraphrase here. This well-known design guideline is often held up as a criterion of object-oriented program quality.

For a class S to follow this principle, the following must be true:

If S is a subclass of T, all operations that are available and meaningful on instances of T are equally available and meaningful on instances of S.

In other words: where you have an instance of T, you can also use an instance of S.

Let’s return to the shape example and these two classes that we defined earlier:

open class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
  def area = this.sideLength * this.anotherSideLength
class Square(size: Double) extends Rectangle(size, size)

Does this code follow the Liskov substitution principle? What if we used var instead of val in class Rectangle? Recall: our goal when writing Square was to model the fact that a square is a rectangle whose sides are equal in length.

More in Wikipedia:

On dependencies between classes/traits

The chapter on traits (7.3) already noted the asymmetric relationship between a subtype and a supertype: we define each subtype in terms of its supertype(s), not the other way around. This applies to inheritance, too:

  • In a subclass definition, we record the superclass that the subclass extends. Given that relationship, we can rely on the inherited methods being available for us to use within the subclass. We can write things like this.methodFromSuperclass. We may also use the super keyword to refer to the superclass definition.

  • We do not record the inheritance relationship in the superclass, and the superclass’s code is independent of the subclasses in this respect. We can't call this.methodInSubclass; we can invoke methods on this only if they are common to all instances of the supertype. There is no sub keyword.

If superclasses were dependent on their subclasses, changing those subclasses would necessitate changes in the superclass. That would be quite vexing. For instance: It’s fairly common to extend a superclass defined in a library (e.g., View). There is no way that the library’s creator can know all the possible subclasses that may be created for their class or react to changes in those subclasses.

On the other hand, it’s great that we can add methods to a superclass and those methods will then be available in all extending classes. Often, this works fine, but sometimes, we run into a weakness in inheritance-based program design:

On fragile superclasses

The fragile base class problem refers to situations where it’s impossible to modify a superclass without knowing details about its subclasses.

As one example, consider adding a method to a superclass: what if the new method happens to have the same name as one or more of the methods in the subclasses? We have a serious problem.

In a language such as Scala, our program will break down with a compile-time error that complains about a missing override modifier. In languages that don’t have such a modifier or that make it optional, we may get surprising and erroneous runtime behavior instead.

O1’s follow-on courses will discuss object-oriented design more deeply. In the meantime, you may find the Wikipedia article on fragile base classes interesting; see also composition over inheritance.

Preventing overriding and inheritance with final

If you add the final modifier in front of a def, the method can’t be overridden: instances of all subtypes will inherit the method as is. An attempt to override it will bring a compile-time error.

In front of class, the same final keyword disallows inheritance from that class altogether. (Cf. sealed, which limits direct inheritance to a single file.)

When used appropriately, the final modifier may make programs easier to comprehend and prevent inappropriate use of a class. Some programs’ efficiency is improved by final, since the compiler doesn’t need to worry about overriding.

Many of the classes in the standard Scala API are final.

Union types (such as Int|String)

Earlier, we saw that the static type Any covers all sorts of values, as shown here:

val miscValues: Vector[Any] = Vector(-123, "llama")miscValues: Vector[Any] = Vector(-123, llama)

Let’s explore what happens if we leave out the type annotation Vector[Any].

val miscValues = Vector(-123, "llama")miscValues: Vector[Int | String] = Vector(-123, llama)
val firstElem = miscValues.headval firstElem: Int | String = -123

Scala infers that the type is Vector[Int|String], which essentially means a vector whose each element is either an Int or a String.

Just Int|String is also a type. This vector’s head returns a value whose static type is “either an integer or a string”. A type like this is known as a union type (yhdistetyyppi).

What can you do with a value whose static type is Int|String? Not very many things. Only those operations are available that that are valid on both integers and strings:

firstElem.length-- [E008] Not Found Error:
  value length is not a member of Int | String
firstElem.abs-- [E008] Not Found Error:
  value abs is not a member of Int | String

length is defined for String objects, and abs is defined for integers, ...

... but neither of the methods is available on both Strings and Ints, so commands like these generate a compile-time error.

However, if you wish to make a decision based on a value’s dynamic type, the match command works nicely with union types:

firstElem match
  case number: Int    => number.abs
  case string: String => string.lengthres13: Int = 123

There is no need to add any additional cases to cover the possibility of other kinds of elements, since an Int|String value is guaranteed to be either an Int or a String.

You may also use an union type explicitly in code. For example, you may mark a variable as having a union type, as shown below.

def process(numberOrString: Int | String) =
  numberOrString match
    case number: Int    => number.abs
    case string: String => string.lengthdef process(numberOrString: Int | String): String
Vector(-123, "llama").map(process)res14: Vector[Int] = Vector(123, 5)

Summary of Key Points

  • You may extend not only traits but regular classes, too. Subclasses, which represent more specific concepts, then inherit the generic properties defined in the superclass.

  • You may define a class as abstract, in which case it can contain abstract methods and variables just like a trait can. An abstract class can’t be instantiated directly but only via its subclasses.

  • Traits and abstract superclasses have much in common, but there are differences, too. The exact differences depend on the programming language. (Some languages don’t have these as separate concepts.)

    • In O1, we’ll be using traits. For purposes of the course, pretty much all you need to know about abstract classes is that they’re similar to traits.

  • In Scala, all classes are part of a hierarchy whose base is the API class Any.

  • Links to the glossary: inheritance, subclass, superclass, type hierarchy, Any; abstract class; static type, dynamic type; open class, sealed class; final.

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 and so 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó, 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, Juha Sorva, and Jaakko Nakaza. 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; 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 has been designed and implemented by various students in collaboration with O1’s teachers.

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...