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
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.
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
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 |
Example |
---|---|
The supertype is represented by a trait. |
|
Each subtype inherits that trait. |
|
A trait may define abstract methods and variables. |
|
A trait cannot be instantiated directly. |
One does not simply write |
A class (or trait) may directly inherit multiple traits. |
|
A trait cannot pass constructor parameters to its supertype(s). |
|
And here is a similar table about inheritance and the example of Rectangle
as a
superclass for Square
:
When inheriting from a regular |
Example |
---|---|
The supertype is represented by a regular class (which is best to mark as open, if meant for inheritance). |
|
Each subtype inherits that superclass. |
|
An ordinary class cannot define abstract methods or variables. (But some classes can. See below.) |
All the methods on |
A superclass can be instantiated directly. |
|
(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 |
|
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 aVector
.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 thatItem
class. It’s missing theopen
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 variablename
within theContainer
subclass. That variable is already defined in the superclass. It’s perfectly fine to giveContainer
’s constructor parameter the namename
, though.Note that what you
override
in this assignment isn’t the generictoString
fromAnyRef
, as in many other programs, but thetoString
implementation inItem
.Can you implement the
toString
inContainer
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, thecontents
method must return aVector
. If you make use of a buffer, addimport 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 |
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.
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)
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
:
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. AnyVal
s must be immutable
and conform to other strict conditions as well. When used in the
right places, AnyVal
s 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 View
s: 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 Animal
s 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
andSpitting
; andimplements the abstract method
numberOfHumps
in theHumped
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
“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 thesuper
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 onthis
only if they are common to all instances of the supertype. There is nosub
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 String
s and Int
s, 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 case
s 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.
This is a regular
class
, which can be instantiated directly, but it is open (avoin) for extension. By adding the keywordopen
, we indicate that this class is designed to be freely usable as a superclass (yliluokka): other classes may inherit from it.