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 4.4: Exercises in Not Existing
About This Page
Questions Answered: Could I practice writing classes some more,
please? How can I make use of the Option
type? And match
?
Topics: The constructs from the previous chapter.
What Will I Do? The first half of the chapter is a series of small programming assignments. The second half contains a larger assignment and some voluntary reading.
Rough Estimate of Workload:? Three or four hours.
Points Available: A140.
Related Modules: Miscellaneous, Stars (new), and Football3 (new). IntroOOP and MoreApps feature in optional assignments.
Assignment: Improve Class VendingMachine
You may recall that after we created class VendingMachine
in Chapter 3.5, we raised
some questions about the quality of its sellBottle
method.
Locate VendingMachine
in module Miscellaneous and modify it. Edit sellBottle
so that
it no longer returns minus one to signify a failed purchase. Instead, the method’s return
type should be Option[Int]
; the method should return None
if no bottle was sold and
the amount of change wrapped in a Some
object if a bottle was sold.
A+ presents the exercise submission form here.
Assignment: Fix Class Member
The same module contains class o1.people.Member
, a separate example. Examine its
program code and documentation. You’ll find that the code doesn’t match the docs: the
methods isAlive
and toString
are missing. Write them.
The match
command is one way to solve this assignment, but if you use a couple of the
methods on Option
, you should be able to come up with a still simpler solution. The
methods were introduced in the previous chapter (4.3).
A+ presents the exercise submission form here.
Assignment: Implement Class Passenger
Task description
A class Passenger
is listed in the documentation for o1.people
. No matching code
has been given, however. Implement the class.
Instructions and hints
The class uses another class,
TravelCard
. That class has been provided for you. Use it as is; don’t change it.The documentation details
Passenger
’s constructor parameters and their corresponding public instance variables. You won’t need to define any private instance variables for this class.As stated in the Scaladocs, passengers should have an instance variable of type
Option[TravelCard]
; that is, each passenger has zero or one travel cards.Option
works as a wrapper for our custom classTravelCard
.You need to create a file for class
Passenger
withino1.people
. Here’s how:Right-click the package in IntelliJ’s Project view and select New → Scala Class. A small dialog pops up.
IntelliJ will set up the file for you after you enter some additional information in the dialog. At the Name prompt, enter
Passenger
and press Enter.A new file shows up in the editor, with a bit of starter code.
Refer to Chapter 4.3 for tools that you can use to implement the methods.
If you find yourself struggling with how to access
isValid
, you may want to check out the additional hints below.
A first hint about isValid
You have a value of type Option[TravelCard]
.
That Option
object does not have an isValid
variable; the TravelCard
object that it may contain
does. The expression`this.card.isValid` thus does not
work.
Handle the Some
and None
cases separately.
Access isValid
on the Some
object’s contents.
A further hint about isValid
From Chapter 4.3: When you handle the Some
case
using match
, you can extract the Some
object’s
contents into a variable and pick a name for the
variable.
Use that variable to access isValid
, as in
variableName.isValid
.
A+ presents the exercise submission form here.
Assignment: Improve Class Order
There’s nothing new about the problem below, but it provides further practice on
Option
s and match
. We recommend it especially if you had difficulty with the above
assignments. You can also come back to practice on this problem if you run into trouble
later on.
Additional practice
Return to class Order
in module IntroOOP.
There was a tiny optional assignment in
Chapter 2.6 where the description
method in class Order
was replaced by a toString
method.
If you didn’t do that then, do it now: rename description
to
toString
and write override
in front. Also do this for
class Customer
.
Now edit the class as follows:
Add a constructor parameter
address
with the typeOption[String]
. Also introduce a correspondingval
instance variable. This variable will to store a postal address that is (possibly) assosiated with the order.Add a parameterless, effect-free method
deliveryAddress
. It should return aString
that indicates where the order should be delivered. This will be either the address associated with the order, if there is one, or the orderer’s personal address, if the order isn’t associated with an address.Edit the
toString
method so that there’s an additional bit at the end: a comma and a space", "
followed by either"deliver to customer's address"
or"deliver to X"
, where X is the address associated with the order.
A+ presents the exercise submission form here.
Assignment: Convenient Keyboard Control
The next optional assignment is meaningful only if you have done the Trotter
assignment
from Chapter 3.6. Of course, you could do that assignment now if you didn’t already.
Direction.fromArrowKey
Package o1
contains not just the class Direction
but also a singleton object
of the same name. The singleton object has a convenient
method fromArrowKey
, whose behavior is illustrated below.
val exampleKey = Key.UpexampleKey: o1.Key.Value = Up Direction.fromArrowKey(exampleKey)res0: Option[Direction] = Some(Direction.Up) Direction.fromArrowKey(Key.X)res1: Option[Direction] = None
As you see, the method returns the direction that corresponds to the given key
on the keyboard. It wraps the return value in an Option
, because only some keys
correspond to a direction.
Edit TrotterApp
’s onKeyDown
method in module MoreApps. The method should do the
same job as before but you can write a simpler and less redundant implementation
with fromArrowKey
.
A+ presents the exercise submission form here.
Assignment: Star Maps (Part 1 of 4: Basic Star Info)
Introduction: stars and their coordinates
Let’s write a program that displays star maps: views of the night sky. A star map contains a number of stars; stars may also be linked together to form constellations.
In this first part of the assignment, we won’t be drawing anything yet. We’ll begin by creating some tools for representing individual stars.
Fetch the Stars module. For now, we’ll concentrate on classes StarCoords
and Star
:
you use the former, which is already implemented, to implement the latter. Study the two
classes’ documentation; don’t mind the other classes now.
As indicated in the Scaladocs, our program needs to work on two different sorts of two-dimensional coordinates:
A star’s location on a two-dimensional star map is represented as a
StarCoords
object.For this, we use a coordinate system like the one you know from math class, with values of y increasing towards the top.
We assume that all values of x and y have been normalized so that they fall within the interval [-1.0...+1.0]. (See the illustration.) The two coordinates represent a star’s location in the visible sky independently of the size of any picture that may depict the sky.
On the other hand, you can use the method
toImagePos
of aStarCoords
instance to produce aPos
that represents the star’s location within a particularPic
. These coordinates increase, like the otherPos
coordinates that we’ve used, right- and downward from the top left-hand corner. They indicate which pixel the star’s center should appear at within a largerPic
of the entire sky that has a known width and height.
Task description
Read the above and the Scaladocs. Make sure you understand the two coordinate systems we need. Make sure you understand what
toImagePos
inStarCoords
accomplishes.Then implement the missing methods of class
Star
so that their behavior meets the Scaladoc specification.
Instructions and hints
Here is an example of how your
Star
class should work:val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 4.61, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02) unnamedStar.posIn(rectangle(100, 100, Black))res2: o1.world.Pos = (99.7386,48.8418) unnamedStar.posIn(rectangle(200, 200, Black))res3: o1.world.Pos = (199.4772,97.6836) val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)
You don’t actually have to implement the required math yourself if you make good use of
StarCoords
.You also don’t need to round a star’s coordinates even though
toString
returns them in rounded form. ThetoString
method inStarCoords
already does that for you, so use it.
Formatting Double
s within String
s
You may want to take a look at toString
in class StarCoords
,
which rounds the coordinates to two decimal points. That method
uses an f
notation that you can read more about in an article
on different forms of string interpolation.
A+ presents the exercise submission form here.
Assignment: Star Maps (Part 2 of 4: Seeing Stars)
Task description
The Stars module also includes the singleton object o1.stars.io.SkyPic
, whose methods
we’ll use to generate pictures of star maps.
In this short assignment, we’ll focus on placeStar
, a method that takes a picture and
a star as parameters. The method returns a modified version of the picture. That new
picture is identical to the original except that it has a picture of the star drawn on
top.
For instance, let’s say we want a picture that contains pictures of these two stars:
val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 4.61, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02) val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)
The following code should do the trick.
val darkBackground = rectangle(500, 500, Black)darkBackground: Pic = rectangle-shape val skyWithOneStar = SkyPic.placeStar(darkBackground, unnamedStar)skyWithOneStar: Pic = combined pic val skyWithTwoStars = SkyPic.placeStar(skyWithOneStar, namedStar)skyWithTwoStars: Pic = combined pic skyWithTwoStars.show()
placeStar
lacks an implementation. Read its specification in the Scaladocs and implement
it in SkyPic.scala
where indicated.
Instructions and hints
This part-assignment should be easy if you use the tools introduced in Part 1 above.
After writing the method, you may reflect on what we have and have not accomplished
so far: we can now display individual stars in a star map, but it would take a lot of
manual work to display a map with more than a sprinkling of stars. It would be much more
convenient if we could load star data into our program from a repository of some sort.
And that is indeed what we will do soon enough. (Feel free to take a look at the folders
test
and northern
and especially the stars.csv
files in those folders. We’ll return to
this program in Chapter 5.2.)
A+ presents the exercise submission form here.
Assignment: Football3
Our football-scores application (of Chapter 3.5) was already rewritten once (in Chapter 4.2), and now we’re going to mess with it again.
Task description
Fetch Football3. Study its documentation. The Match
class is now somewhat different
than before, and there is a new class, Season
.
Implement the two classes so that they meet the new specification.
Instructions and hints
You can copy and paste your
Match
implementation from Football2 into Football3 and work from there. The only changes you need to make to the old code are in the winner-related methods:In addition to
winnerName
, there is a new method named simplywinner
, which returns anOption
.winningScorerName
no longer exists. It’s replaced bywinningScorer
, which returns anOption
.(Once you have
winner
working, you can use it to simplifywinnerName
.)
Remember:
Match
with a capital M is a programmer-chosen name for a class that represents football matches.match
is a Scala command (that can’t be used as a name; it’s a reserved word). Mix up these two, and you may get some interesting error messages.You can again use
FootballApp
for testing. Once you’ve implementedSeason
, the app’s main window will display season statistics and a list of matches.For implementing
Season
you may find it useful to review Chapters 4.2 and 4.3 and the GoodStuff application. There are a few optional hints available below.
A generic hint for the biggestWin
method
GoodStuff’s Category
class kept track of the experience with
the highest rating. The Season
class, in comparison, keeps track
of the match with the biggest win margin. In this respect, these
two programs are very similar.
For working out the biggest win, you can use an instance variable
of type Option
as a most-wanted holder, much like we did in class
Category
. You can update that variable in addResult
much as the
Category
class does in its addExperience
method.
Hints for comparing Match
es to one another
Did you notice that goalDifference
can return a negative
number? The match with the biggest win margin is the one whose
goalDifference
’s absolute value is the greatest.
To compute absolute values, recall that you have the function
scala.math.abs
available.
You can either compare the matches in addResult
itself or you
can define a helper method to perform the comparison and call
that method.
A hint for fixing IndexOutOfBounds
errors that crash your program
An index-out-of-bounds error means that you’ve tried to access a collection index that is either too large or (probably in this case) too small.
Did you remember to consider the possibility that a season may have no matches at all yet?
If this didn’t help enough, you may want to check the next hint as well.
A hint for matchNumber
and latestMatch
These two methods should return a match within an Option
wrapper. To achieve that, you don’t need an if
expression or
anything complicated. See Chapter 4.3 for a method that lets
you access an element of a collection safely as an Option
.
By the way, did you notice that latestMatch
is a special case
of matchNumber
? You can implement the former by calling the
latter.
A+ presents the exercise submission form here.
Something to think about
The documentation goes out of its way to say that matches added to
a Season
are assumed to have finished and no more goals will be
added to those matches. What happens if you go against this
assumption? What would it take to reimplement the program so that
we prevent that from happening?
Further Reading: The Flexible match
Command
The boxes below tell you more about the match
command, which we’ve used only for
manipulating Option
objects. This additional information isn’t required for O1 but
should interest at least those readers who have prior programming experience and wish
to explore Scala constructs in more depth. Beginners can perfectly well skip this section
and learn about these topics at a later date.
match
is a pattern-matching tool
Here is the general form of the match
command:
expression E match case pattern A => code to run if E’s value matches pattern A case pattern B => code to run if E’s value matches pattern B (but not A) case pattern C => code to run if E’s value matches pattern C (but not A or B) And so on. (Usually, you’ll seek to cover all the possible cases.)
... so-called patterns (hahmo) that define different
cases. For now, we have defined only cases with None
and
Some
patterns, but many more kinds of patterns are possible.
Some of them are introduced below.
Primitive match
ing on literals
Suppose we have an Int
variable called number
. Let’s use match
to examine
the value of the expression number * number * number
.
val cubeText = number * number * number match
case 0 => "number is zero and so is its cube"
case 1000 => "ten to the third is a thousand"
case otherCube => "number " + number + ", whose cube is " + otherCube
match
checks the patterns in order until it finds one that
matches the expression’s value. Here, we have a total of
three patterns.
Even a simple literal can be used as a pattern. Here, we’ve used
a couple of Int
literals. The first case is a match if the cube
of number
equals zero; the second matches if the cube equals one
thousand.
You can also enter a new variable name as a pattern; here, we’ve
picked the name otherCube
. Such a pattern will match any value;
in this example, the third case will always be selected if the
cube wasn’t zero or one thousand.
Whenever such a pattern matches, you get a new local variable that stores the actual value that matched the pattern. You can use the variable name to access the value.
Below is a similar example where we match on Boolean
literals rather than Int
s.
These two expressions do the same job:
if number < 0 then "negative" else "non-negative"
number < 0 match
case true => "negative"
case false => "non-negative"
We can accomplish all that by chaining if
s and else
s (Chapter 3.4), too.
Matching on literals and variables has not yet demonstrated the power of match
.
But see below.
Question from student: Is match
roughly the same as Java’s switch
?
Java and some other programming languages have a switch
command that selects among
multiple alternative values that an expression might have. Scala’s match
is similar
in some ways. However, switch
can select only a case that corresponds to a specific
value (as in our cubeText
example), whereas match
provides a more flexible
pattern-matching toolkit. Perhaps the most significant differences are that match
can:
make a selection based on an object’s type; and
“take apart” the object and automatically extract parts of it into variables defined in the pattern.
Examples of both appear later in this chapter.
Guarding a case with a condition
You can associate a pattern with an additional condition (a pattern guard) that needs to be met for a match to happen.
val cubeText = number * number * number match
case 0 => "the number is zero and so is its cube"
case 1000 => "ten to the third is a thousand"
case other if other > 0 => "positive cube " + other
case other => "negative cube " + other
The condition narrows down the case: we select this branch only
if the value is greater than zero (and doesn’t equal 1000, which
we already covered in another case). Note that we use the familiar
if
keyword, but this isn’t a standalone if
command.
The last case will be selected only if the value is non-zero and not 1000 or any other positive number.
Underscores in patterns
An underscore pattern means “anything” or “don’t care what”. Here are a couple of examples.
number * number * number match
case 0 => "the number is zero and so is its cube"
case 1000 => "ten to the third is a thousand"
case _ => "something other than zero or thousand"
The underscore pattern matches any value and is selected if neither of the two preceding cases is. We could have written the name of a variable here (as we did in earlier examples), but if we have no use for the variable’s value, an underscore will do.
match
ing on data types
In the preceding examples, the patterns corresponded to different values of the same type. In this example, the patterns match values of different types:
def experiment(someSortOfValue: Matchable) =
someSortOfValue match
case text: String => "it is the string " + text
case number: Int if number > 0 => "it is the positive integer " + number
case number: Int => "it is the non-positive integer " + number
case vector: Vector[?] => "it is a vector with " + vector.size + " elements"
case _ => "it is some other sort of value"
Our example function’s parameter has the type Matchable
, which
means that we can pass more or less any value as a parameter.
(Anything that can be processed with match
goes; this covers
nearly all Scala classes.)
The patterns have been annotated with data types. Each of these patterns matches only values of a particular type.
The variables defined in the patterns have the corresponding
type. For example, the variable vector
has the type Vector
,
so we can use the variable to call the matching vector’s size
method.
Using match
to take apart an object
One of match
’s most appealing features is that you can use it to destructure the object
that matches a pattern, extracting parts of it into variables. A simple example is the
“unwrapping” of a value stored within an Option
:
vectorOfNumbers.lift(4) match
case Some(wrapped) => "the number " + wrapped
case None => "no number"
The pattern defines the structure of the matched object:
a Some
will have some value inside it. That value is
automatically extracted and stored in the variable wrapped
.
This feature of match
can be combined with the others listed above. Below, we take
apart an Option
and, at the same time, try to match its possible contents to one of
several cases:
vectorOfNumbers.lift(4) match
case Some(100) => "exactly one hundred"
case Some(wrapped) if wrapped % 2 == 0 => "some other even number " + wrapped
case Some(oddNumber) => "the odd number " + oddNumber
case None => "no number at all"
You can destructure an object in this fashion only if the class defines how to do that
for objects of that type. Many of Scala’s library classes come with such a definition,
Some
included. Similarly, O1’s own library defines that a Pos
object can be
destructured into two coordinates:
myPos match
case Pos(x, y) if x > 0 => "a pair of coordinates where x is positive and y equals " + y
case Pos(_, y) => "some other pair of coordinates where y equals " + y
“If it’s a Pos
, two numbers can be extracted from it. Store
them in local variables x
and y
. If x
is positive, this
case matches.”
“Exctract two numbers from the Pos
. Discard the first and
store the other in a variable y
.” Any Pos
will match this
pattern and this branch will be chosen every time if the first
one isn’t.
So how do you define how objects of a particular type can be taken apart? The easiest way is to turn a class into a so-called case class (tapausluokka). Here’s a simple example:
case class Album(val name: String, val artist: String, val year: Int):
// ...
Apart from the extra case
, this is like a regular class
definition.
The constructor parameters of a case class also perform a
second role: they define how to destructure objects of this
type. The name, artist, and year of any album object can be
extracted while match
ing.
You can then use the case class like so:
myAlbum match
case Album(_, _, year) if year < 2000 => "ancient"
case Album(name, creator, released) => creator + ": " + name + " (" + released + ")"
Our pattern definitions use the case class’s name, and their structure matches the class’s constructor parameters.
Search online for Scala pattern matching and Scala case class for more information. As noted, you aren’t required to use these language features in O1.
Further Reading: Option.get
This optional section introduces a method on Option
s that can seem deceptively handy
but that you should avoid.
The dangerous get
method
One way to open up an Option
wrapper is to call its parameterless get
method.
Try it.
We could have used get
instead of match
to implement addExperience
in
class Category
:
def addExperience(newExperience: Experience) =
this.experiences += newExperience
val newFave =
if this.fave.isEmpty then
newExperience
else
newExperience.chooseBetter(this.fave.get)
this.fave = Some(newFave)
isEmpty
checks whether an old favorite exists.
We get
the old favorite from its wrapper. Since we do this
only in the else
branch, we know we’re not dealing with
None
.
However, if we use get
, we’re faced with some of the same problems that we had with
null
: if we call get
on None
, our program crashes. It’s up to the programmer to
ensure that get
is only called on a Some
. This is easy to forget.
As one student put it:
Can’t open the wrapper if there’s no candy.
At least you shouldn’t just rush to open a wrapper, as get
does. It’s better to
be prepared for possible disappointment and avoid the tears.
It’s good to know that get
exists; you may see it used in programs written by
others. Avoid using it yourself. There is always a better solution such as match
,
getOrElse
, or one of the methods from Chapter 8.4.
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, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 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 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.
match
can examine the value of any expression. In technical terms, this examination is a form of pattern matching (hahmonsovitus): the expression’s value is compared to...