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 8.4: Robots and Options
About This Page
Questions Answered: How can I use Option
more deftly?
Let’s keep working on the Robots app of Chapter 8.3, shall we?
Topics: Higher-order methods on Option
s. The robot simulator.
What Will I Do? Read and program. There’s a slew of multiple-choice questions to practice on at the end.
Rough Estimate of Workload:? Three hours.
Points Available: B75.
Related Modules: Robots.
Robot Turns
The methods advanceTurn
and advanceFullRound
in RobotWorld
are supposed to let the
robots take their turns in a round-robin fashion. They can do that by calling takeTurn
in class RobotBody
, which instructs a specific robot to act.
First, let’s see how we can implement the latter method. In the given RobotBody
class,
there’s a draft:
def takeTurn() =
if this.isIntact then
// TODO: call the brain's controlTurn method (if there is a brain)
How an intact robot spends its turn depends on which kind of brain has been plugged into it (if any). The robot should call the appropriate method on its brain object.
The following mini-assignment serves both as a recap and as an introduction to the upcoming topics:
When our program needs to do something simple, it would be nice if we could express that behavior simply. That’s one of the reasons why we now take a little break from the robots and learn some new techniques.
A Number-Finding Problem
Consider a simple, separate example. Let’s say we have a vector of integers and we mean to find the first “big” number (defining “big” as over 10000, for example) as well as the first negative number; moreover, our goal is to determine whether or not those numbers are even.
We can use find
to locate the numbers in the vector. It returns a result as an Option
:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val possibleBigNum = numbers.find( _ > 10000 )res0: Option[Int] = None val possibleNegativeNum = numbers.find( _ < 0 )res1: Option[Int] = Some(-20)
We can’t use the modulo operator %
on an Option[Int]
, which is “a number that may
not exist”:
possibleNegativeNum % 2 == 0-- Error: ... value % is not a member of Option[Int]
The problem is the same as in takeTurn
above: we mean to perform out an operation only
in case a particular value exists.
Choosing a result type
There are three different cases: the number was found and is even; the number was found and is odd; or the number wasn’t found so we can’t say anything about its evenness. We need our program to attend to each of these three possible outcomes.
Option[Boolean]
works for representing the result. Let’s use it like this:
If
find
returnsNone
, the result isNone
as well.If
find
returns aSome
that contains an even number, the result isSome(true)
.If
find
returns aSome
that doesn’t contain an even number, the result isSome(false)
.
One way to implement this scheme is to use match
:
An okay solution with match
val possibleBigNum = numbers.find( _ > 10000 )
val bigIsEven = possibleBigNum match
case Some(firstBig) => Some(firstBig % 2 == 0)
case None => None
Like our takeTurn
implementation earlier, this code is a bit verbose considering how
simple our goal is. We can do better. Let’s adopt a new perspective on the Option
class.
Option
as a Collection
An Option
object is a collection: it contains a number of elements of a specific type.
It’s just that it’s a very simple sort of collection: the number of elements is either
zero or one. None
is a collection with zero elements and Some(x)
is a collection
whose only element is x
.
The authors of the Scala API, too, have treated Option
as a kind of collection. The
Option
class is designed so that it has the same methods as other collections do.
Let’s try some methods on a Some
object, a single-element collection.
Some
as a collection
We can call foreach
on a Some
: the parameter function is then executed on
whichever value is wrapped therein.
val experiment = Some("Here I am")experiment: Some[String] = Some(Here I am) experiment.foreach(println)Here I am
filter
returns either the original Some
, or None
:
experiment.filter( _.length < 100 )res2: Option[String] = Some(Here I am) experiment.filter( _.length >= 100 )res3: Option[String] = None
exists
and forall
work as you might expect:
experiment.exists( _.length >= 100 )res4: Boolean = false experiment.exists( _.length < 100 )res5: Boolean = true experiment.forall( _.length < 100 )res6: Boolean = true
map
produces another Some
object that contains a value computed from the value in the
original Some
:
experiment.map( _.toUpperCase + "!!!" )res7: Option[String] = Some(HERE I AM!!!)
None
as a collection
Now let’s try these methods on None
, a zero-element collection. As with other empty
collections, foreach
simply doesn’t do anything:
val experiment2: Option[String] = Noneexperiment2: Option[String] = None experiment2.foreach(println)
filter
and map
always return None
since there’s no element to work with:
experiment2.filter( _.length < 100 )res8: Option[String] = None experiment2.map( _.toUpperCase + "!!!" )res9: Option[String] = None
Similarly, exists
returns false
:
experiment2.exists( _.length >= 100 )res10: Boolean = false experiment2.exists( _.length < 100 )res11: Boolean = false
forall
, on the other hand, always returns true
since any given condition holds for
all of the collection’s zero elements. Or perhaps you prefer this phrasing: since there
are zero elements, there exists no value for which the given condition does not hold.
experiment2.forall( _.length >= 100 )res12: Boolean = true
A Niftier Solution to the Number-Finding Problem
Let’s start again from the following code:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val possibleBigNum = numbers.find( _ > 10000 )res13: Option[Int] = None val possibleNegativeNum = numbers.find( _ < 0 )res14: Option[Int] = Some(-20)
Let’s write a function that checks a given number’s parity and pass that function to map
.
In other words, let’s issue this command: “Check the evenness of any element within the
Option
that find
returned.”
possibleBigNum.map( _ % 2 == 0 )res15: Option[Boolean] = None
Now, if it happens that the Option
is empty, as above, the result is also None
.
On the other hand, if the Option
wrapper contains a value, our code applies the
parity-checking function to that value and gives us the result in a Some
:
possibleNegativeNum.map( _ % 2 == 0 )res16: Option[Boolean] = Some(true)
This solution attends to all three scenarios: we either find an even number, find an odd number, or find nothing.
Above, we used a variable for illustration, but of course the shorter versions below work, too.
numbers.find( _ > 10000 ).map( _ % 2 == 0 )res17: Option[Boolean] = None numbers.find( _ < 0 ).map( _ % 2 == 0 )res18: Option[Boolean] = Some(true)
Example: The tempo
Function
For another example of map
on an Option
, consider this implementation for the tempo
function of Chapter 5.2:
def tempo(music: String) = music.split("/").lift(1).map( _.toInt ).getOrElse(120)tempo(music: String): Int tempo("cccedddfeeddc---/150")res19: Int = 150 tempo("cccedddfeeddc---")res20: Int = 120
We use split
(Chapter 5.2) and lift
(Chapter 4.3).
The first method call returns the two substrings on either side
of the slash character. The second gives us an Option[String]
that contains the substring that follows the slash, if it exists.
Once lift
gives us a possible string, we can use combination of
map
and toInt
as turn it into a possible integer.
Example: Preferences
Our goal: optional settings in user profiles
Imagine we’re working on an application that uses the simple class below for representing
the app’s users’ personal preference settings. As things stand, the application has only
two different settings: 1) the user’s preferred language and 2) whether or not the user
prefers the metric system of units. Each of those settings is optional: a user may have
recorded their preference on them or not. That fact has been modeled with Option
s:
class Preferences(val profile: String, val language: Option[String], val metricSystem: Option[Boolean]):
// add toString etc. here
end Preferences
A couple of usage examples:
val test = Preferences("My preferred settings", Some("English"), None)test: Preferences = lang: English, metric: NOT SET val test2 = Preferences("Some other settings", Some("Finnish"), Some(true))test2: Preferences = lang: Finnish, metric: true
Let’s also assume that each user either has or hasn’t created a profile for themself.
That is, each user either has or doesn’t have a Preferences
object associated with
them, which we can represent as an Option[Preferences]
. See below for three instances:
Tiina has created a profile and set their preferences, Fang also has a profile but hasn’t
stated their language preference, and Ben has no profile at all:
val tiinasPreferences = Some(Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true) val fangsPreferences = Some(Preferences("Fang's profile", None, Some(true)))fangsPreferences: Some[Preferences] = Some(lang: NOT SET, metric: true) val bensPreferences: Option[Preferences] = NonebensPreferences: Option[Preferences] = None
Given that information, how can we generate a String
that tells us which language the
app’s GUI should use for each of these users? If a user has a profile that names a
language, we’d like to use that. If the user either doesn’t have a profile or their
profile doesn’t specify a preferred language, we’d like to use English as the default
language.
Let’s sketch out a solution in the REPL.
Towards a solution
To determine which language to use for Tiina, we might try this first:
tiinasPreferences.language-- Error: ... value language is not a member of Some[Preferences]
That doesn’t work, since a user may not have a profile. A Some
doesn’t have a language
,
but its contents do. Let’s use map
again:
tiinasPreferences.map( _.language )res21: Option[Option[String]] = Some(Some(Finnish))
The language setting is nested in two maybes: “We may have
a profile that may specify a language.” Hence the type
Option[Option[String]]
.
Tiina does have a profile: tiinasPreferences
isn’t None
but
a Preferences
object wrapped in a Some
. That object’s language
isn’t None
either but the string "Finnish"
wrapped in a Some
.
Therefore, when you map
for the language
of a Preferences
object, you get a Some
inside another.
flatten
eliminates the nesting:
tiinasPreferences.map( _.language ).flattenres22: Option[String] = Some(Finnish)
In what is hopefully a familiar maneuver by now, we can combine map
and flatten
into
flatMap
:
tiinasPreferences.flatMap( _.language )res23: Option[String] = Some(Finnish)
This gives us Tiina’s language preferences within an Option[String]
. Some(Finnish)
informs us that there is a language preference and it is for Finnish.
Our goal was to use the user’s preferred setting if available and a default language
otherwise. getOrElse
does the job:
val tiinasLanguage = tiinasPreferences.flatMap( _.language ).getOrElse("English")tiinasLanguage: String = Finnish
The solution as a function
Let’s abstract our example into a function that uses a (possibly missing) user profile to determine the GUI language:
def chooseLanguage(prefs: Option[Preferences]) = prefs.flatMap( _.language ).getOrElse("English")chooseLanguage(prefs: Option[Preferences]): String
We can apply this function to each of our three example users:
val tiinasPreferences = Some(Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true) val fangsPreferences = Some(Preferences("Fang's profile", None, Some(true)))fangsPreferences: Some[Preferences] = Some(lang: NOT SET, metric: true) val bensPreferences: Option[Preferences] = NonebensPreferences: Option[Preferences] = None val tiinasLanguage = chooseLanguage(tiinasPreferences)tiinasLanguage: String = Finnish val fangsLanguege = chooseLanguage(fangsPreferences)fangsLanguege: String = English val bensLanguage = chooseLanguage(bensPreferences)bensLanguage: String = English
Yes, we could have used match
, but not as neatly
This works:
def chooseLanguage(prefs: Option[Preferences]) =
val languagePref = prefs match
case Some(existingPrefs) => existingPrefs.language
case None => None
languagePref match
case Some(pref) => pref
case None => "English"
An additional example
Here’s the toString
method for Preferences
. It contains an additional
example of calling map
on an Option
.
class Preferences(val profile: String, val language: Option[String], val metricSystem: Option[Boolean]):
override def toString =
def describe(name: String, value: Option[String]) =
name + ": " + value.getOrElse("NOT SET")
describe("lang", this.language) + ", " + describe("metric", this.metricSystem.map( _.toString ))
end Preferences
Example: Passenger
In Chapter 4.4, you wrote a Passenger
class that relied on another class, TravelCard
.
Passenger
’s canTravel
method was implemented using match
:
class Passenger(val name: String, val card: Option[TravelCard]):
def canTravel = this.card match
case Some(actualCard) => actualCard.isValid
case None => false
end Passenger
Now you know that it’s also possible to write:
class Passenger(val name: String, val card: Option[TravelCard]):
def canTravel = this.card.exists( _.isValid )
end Passenger
The code succinctly expresses what we mean it to say: a passenger can travel if they have a card that is valid.
More generally, we can say that these two snippets of code accomplish the same thing:
x match
case Some(content) => someCondition(content)
case None => false
x.exists(someCondition)
Assume x
refers to some object of type Option
.
someCondition
is a function that operates on the
Option
’s contents and returns a Boolean
.
exists
is just one Option
method that you can use instead where you might otherwise
use match
. You’ll find opportunities to use these methods as we now return to Robots:
Robot Movements
In Chapter 8.3, you did the first two parts of the Robots assignment. Now on to the next two.
Here are couple of hints that are useful both now and in later parts of the assignment:
My motto for this assignment was “Option
is a collection.”
It really helped.
I noticed that exists
, forall
, and foreach
are fantastic
on Option
s.
Try to solve the assignments in this chapter and the next without match
ing on Option
s.
Use the higher-order methods instead.
Oh, and there’s one warning sign that I need to put up before you get started:
About the get
method on Option
s
As the optional material in Chapter 4.4 mentioned, Option
objects have a method that is named simply get
. You may also
run into this method if you search the web for Scala examples.
The get
method returns the contents of the “wrapper” but crashes
the program with a runtime exception if there’s no content to be
found. The earlier chapter warned against using the method, and
we haven’t been using it in any of our example programs. Nevertheless,
some O1 students have been invoking this method in their code. Some
of these students have run into trouble as a result; others have
not, but they too should attend to this info box.
The warning from Chapter 4.4 applies to these robot assignments
and more generally as well. Do not use the get
method on
Option
s; use other methods instead. Although get
may seem
convenient, using it is a bad habit. The method breaks the
type safety of the language and is suitable for certain special
circumstances only.
In O1, do not submit any code that uses Option
’s get
method.
Frequently asked: But my get
is in an if
— surely it’s not banned then?
Say we have a variable possibleWord
that is of type Option[String]
.
Now, it’s not impossible to use it like this:
val output = if possibleWord.isDefined then possibleWord.get else "no word"
println(output)
if possibleWord.isDefined && munOption.get.length > 10 then
println("found a long word")
That word “works”, so to speak, but please don’t write code like that. There’s always a better way. In our example, this is the way:
val output = possibleWord.getOrElse("no word")
println(output)
if possibleWord.exists( _.length > 10 ) then
println("found a long word")
One key benefit here is that opportunities for common human mistakes are eliminated, as we don’t call any method that may crash our program, nor do we have to remember to do any checks before calling methods. Moreover, our code is simpler to write and easier to read, at least once we’re used to this style.
In well-written Scala, get
has barely any reasonable uses.
It introduces an element of risk to our programs while achieving
nothing of note.
Robots, Part 3 of 9: turns and Spinbot
s
Implement
takeTurn
as outlined at the top of this chapter. This should be quite simple as long as you pick the right tool for dealing with the optional robot brain.RobotBody
also needs thespinClockwise
method. Implement it.CompassDir
objects have a parameterlessclockwise
method, which returns the direction that’s 90 degrees clockwise from the original. (E.g.,East.clockwise
returnsSouth
.) Feel free to use it.
Examine
RobotBrain
’s documentation and Scala code. Notice in particular the methodscontrolTurn
(which you called fromRobotBody
) andmoveBody
. You don’t need to make any changes to this trait at this point. Instead, you’ll next implement theSpinbot
subtype.In class
Spinbot
, themoveBody
method doesn’t do anything. Implement the method.In class
RobotWorld
, the methodsrobotWithNextTurn
,advanceTurn
, andadvanceFullRound
are missing. Implement them. Use higher-order methods on collections where appropriate.Run your code and see if
Spinbot
s work now. Also try breaking down a robot in the GUI and make sure the robot no longer spins. Submit your solution after testing.
A+ presents the exercise submission form here.
Robots, Part 4 of 9: preparing to move robots
The RobotBody
and RobotBrain
classes are missing several small methods that
examine a robot’s surroundings and influence robots’ movement.
In class
RobotBody
, implement theneighboringSquare
method.In class
RobotBrain
, implementisStuck
,locationInFront
,squareInFront
,robotInFront
, andadvanceCarefully
.
A+ presents the exercise submission form here.
We’ll continue with the robots in the next chapter. But first, answer the questions below
to practice on Option
s and check your understanding.
Practice on Option
s
Tidbit: Another way to write a match
The optional materials in Chapter 4.4 briefly introduced some
features of Scala’s match
command. One of those was that you can
attach a condition to a case
by adding the if
keyword there.
So these two accomplish the same thing:
x match
case Some(wrapped) => if myCode(wrapped) then x else None
case None => None
x match
case Some(wrapped) if myCode(wrapped) => x
case anyOtherValue => None
What was the lesson there?
Most of the things you might wish to do with an Option
are simple if you pick the
right method for the task. You can use higher-order methods on Option
s to write code that
is compact and yet understandable — at least if you can expect the reader to be familiar
with these common methods.
Whether you select with if
and match
or use these higher-order methods is up to you.
Either way, you should be aware of both alternatives.
Using a for
loop to process an Option
, as in the last question above, is often
convenient, too.
How are your robots?
If you didn’t yet use the Option
methods in your Robots
implementation, do that now; you’ll learn something. Can
you eliminate all match
es?
More practice (and the treacherous get
method)
If you didn’t get your fill yet, you can continue practicing Option
al thinking by
answering the following questions that again ask you to pick the method that corresponds
to the given piece of code.
Each of these snippets features the get
method, which simply returns the contents of
an Option
wrapper, or crashes at runtime on None
. As mentioned in Chapter 4.4, and
again in this chapter, you should abstain from using this method, as it causes bugs
and represents bad programming style. We’ve used get
below not to suggest that you
follow suit but the opposite: to show you that you don’t need this method.
Additional questions in the same style
More questions to read and think about
So,
Option
is a collection. How about using a regular collection instead? For instance, you might use a buffer with at most one element. Why or why not do that?Find out what Scala’s
Either
class is and how it is similar to and different fromOption
. Also look up the this trio of classes:Try
,Success
,Failure
.Perhaps you’d like to read what has been said about the possible overuse of
Option
. Can you find any example code in this chapter that might be better expressed differently? On a related note, you may find the Null Object design pattern interesting.
Summary of Key Points
An
Option
object is a collection of elements. It contains zero or one elements.Option
objects have the methods you know from other collection types:foreach
,exists
,map
,flatMap
, etc. With these methods, “values that are maybe there” are easier to work with.Sure, you can use
match
, too, but you’ll probably prefer the methods once you get to know them.You can also process an
Option
with afor
loop.Do not use the
get
method onOption
s, however.
Glossary:
Option
, collection, higher-order function.
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, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, 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 for this page
Joonatan Honkamaa and Otto Seppälä adjusted RobotWorld
’s turn logic for efficiency.
A robot body that’s broken or stuck doesn’t do anything on its turn. This check is already there.