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. Myös suomenkielisessä materiaalissa käytetään ohjelmaprojektien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.
Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 8.2: 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.1, 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:? Under two hours.
Points Available: B45.
Related Projects: Robots.
Robot Turns
The methods advanceTurn
and advanceFullTurn
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. There’s a draft in the given
RobotBody
code:
def takeTurn() = {
if (this.isMobile) {
// TODO: call the brain's moveBody method (if there is a brain)
}
}
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<console>:10: 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 an 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) => 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, if you prefer: 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 4.5:
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
split
(Chapter 4.5) and lift
(Chapter 4.2).
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.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 profileName: String, val language: Option[String], val metricSystem: Option[Boolean]) {
// add toString etc. here
}
A couple of usage examples:
val test = new Preferences("My preferred settings", Some("English"), None)test: Preferences = lang: English, metric: NOT SET val test2 = new 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 themselves.
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(new Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true) val fangsPreferences = Some(new 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<console>:20: error: value language is not a member of Some[Preferences] tiinasPreferences.language ^
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
, just like we did in the number-finding example:
tiinasPreferences.map( _.language )res21: Option[Option[String]] = Some(Some(Finnish))
Option[Option[String]]
.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(new Preferences("Tiina's profile", Some("Finnish"), Some(true)))tiinasPreferences: Some[Preferences] = Some(lang: Finnish, metric: true) val fangsPreferences = Some(new 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
An additional example
Here’s the toString
method for Preferences
. It contains an additional
example of calling map
on an Option
.
class Preferences(val profileName: 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 ))
}
}
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"
}
}
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
}
}
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 )
}
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)
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 otherwse
use match
. You’ll find opportunities to use these methods as we now return to the Robots
project:
Robot Movements
In Chapter 8.1, 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:
Option
is a collection.”
It really helped.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.
Robots, Part 3 of 8: 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.- In class
Spinbot
, themoveBody
method doesn’t do anything. Implement it. - In class
RobotWorld
, the methodsnextRobot
,advanceTurn
, andadvanceFullTurn
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.
Robots, Part 4 of 8: restrictions on movement
Some methods are still missing from RobotBody
.
- Implement
neighboringSquare
,canMoveTowards
, andisStuck
. - Submit your solution to Parts 3 and 4.
A+ presents the exercise submission form here.
We’ll continue with the robots in the next chapter. First, though, use the questions
below to practice on and check your understanding of Option
s.
Practice on Option
s
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 know; you’ll learn something. Can you
eliminate all match
es?
Additional practice questions
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 code snippets features the get
method, which simply returns
the contents of an Option
wrapper, or crashes at runtime in case of None
.
As mentioned near the end of Chapter 4.3, you should abstain from using this
method, which is just calling out for a careless mistake. We’ve used get
below
not to suggest that you do so but the opposite: to show you that you don’t need
this method.
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.
- Sure, you can use
- You can also process an
Option
with afor
loop. - 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 that has contributed to this ebook’s design. Thank you!
Weeks 1 to 13 of the ebook, including the assignments and weekly bulletins, have been written in Finnish and translated into English by Juha Sorva.
Weeks 14 to 20 are by Otto Seppälä. That part of the ebook isn’t available during the fall term, but we’ll publish it when it’s time.
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 programmed by Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio have done 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 tools from O1Library (such as Pic
) for simple graphical programming
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+ has been created by Aalto’s LeTech research group and is largely developed by students. The current lead developer is Jaakko Kantojärvi; many other students of computer science and information networks are also active on the project.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.