This course has already ended.

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 Options. 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: B55.

Related Projects: Robots.

../_images/person10.png

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)
  }
}
A robot body that’s broken or stuck doesn’t do anything on its turn. This check is already there.
How a robot spends its turn depends on which kind of brain have been plugged into it (if any). The robot should call the appropriate method on its brain.

The following mini-assignment serves both as a recap and as an introduction to the upcoming topics:

Here are four implementations of takeTurn:

// Version 1:
def takeTurn() = {
  if (this.isMobile) {
    this.brain.moveBody()
  }
}
// Version 2:
def takeTurn() = {
  if (this.isMobile && this.brain.isDefined) {
    this.brain.moveBody()
  }
}
// Version 3:
def takeTurn() = {
  if (this.isMobile) {
    this.brain match {
      case Some(actualBrain) =>
        actualBrain.moveBody()
    }
  }
}
// Version 4:
def takeTurn() = {
  if (this.isMobile) {
    this.brain match {
      case Some(actualBrain) =>
        actualBrain.moveBody()
      case None =>
        // do nothing
    }
  }
}
Which of the following claims is correct?
Now explain the previous answer. Select all the correct claims below.

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 returns None, the result is None as well.
  • If find returns a Some that contains an even number, the result is Some(true).
  • If find returns a Some that doesn’t contain an even number, the result is Some(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 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 Options:

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 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(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))
The language setting is nested ins 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(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 5.1, 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)
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 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:

My motto for this assignment was: “Option is a collection.” It really helped.
I noticed that exists, forall, and foreach are fantastic on Options.

Try to solve the assignments in this chapter and the next without matching on Options. Use the higher-order methods instead.

Robots, Part 3 of 10: turns and Spinbots

  1. 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.
  2. RobotBody also needs the spinClockwise method. Implement it.
  3. In class Spinbot, the moveBody method doesn’t do anything. Implement it.
  4. In class RobotWorld, the methods nextRobot, advanceTurn, and advanceFullTurn are missing. Implement them. Use higher-order methods on collections where appropriate.
  5. Run your code and see if Spinbots work now. Also try breaking down a robot in the GUI and make sure the robot no longer spins.

Robots, Part 4 of 10: restricted movement

Some methods are still missing from RobotBody.

  1. Implement neighboringSquare, canMoveTowards, and isStuck.
  2. 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 Options.

Practice on Options

In one of the examples above, we rewrote the match command in class Passenger by calling exists instead. Each of the following questions similarly gives you a piece of code and asks you to identify a shorter one that accomplishes the same goal.

In all these questions, we’ll assume that myCode is an expression whose type fits that particular question — myCode may have different types in different questions. Moreover, we’ll assume that x is an Option object whose specific type similarly fits the question.

The answers are logical and unambiguous, but you may find the questions challenging nonetheless. It’s possible to work out each answer by reasoning about the code, but feel free to use the REPL as needed. An additional hint: in some of the questions, you’ll need to be alert to the fact that an Option can contain another Option.

x match {
  case None          => false
  case Some(wrapped) => myCode(wrapped)
}

Which of the following accomplishes the same thing?

x match {
  case Some(wrapped) =>
    myCode(wrapped)
  case None =>
    // nothing
}
x match {
  case Some(wrapped) => Some(myCode(wrapped))
  case None           => None
}
x match {
  case Some(wrapped) => if (myCode(wrapped)) x else None
  case None          => None
}
x match {
  case Some(wrapped) => myCode(wrapped)
  case None          => true
}
x match {
  case Some(wrapped) => x
  case None          => myCode
}
if (x.isDefined) x else myCode
x match {
  case Some(wrapped) => wrapped
  case None          => None
}
x.getOrElse(None)
x match {
  case Some(wrapped) => myCode(wrapped)
  case None          => None
}
for (value <- x) {
  myCode(value)
}

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 Options 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 matches?

Additional practice questions

If you didn’t get your fill yet, you can continue practicing Optional 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 in case of None. As mentioned near the end of Chapter 4.4, 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.

if (x.isDefined) Some(myCode(x.get)) else None
x.isEmpty || myCode(x.get)
x != None && myCode(x.get)
if (x.isDefined) {
  myCode(x.get)
}
if (x.isDefined) x.get else None
if (x.isDefined && myCode(x.get)) x else None
if (x.isEmpty || myCode(x.get)) x else None
if (x.isDefined) myCode(x.get) else None

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 from Option. 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 a for 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 developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, 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 behind O1Library’s tools 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+ 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 for this page

Joonatan Honkamaa and Otto Seppälä adjusted RobotWorld’s turn logic for efficiency.

a drop of ink
Posting submission...