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.3: Robots and Options

../_images/person10.png

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)

A robot body that’s broken or stuck doesn’t do anything on its turn. This check is already there.

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:

Here are four implementations of takeTurn:

// Version 1:
def takeTurn() =
  if this.isIntact
    this.brain.controlTurn()
// Version 2:
def takeTurn() =
  if this.isIntact && this.brain.isDefined then
    this.brain.controlTurn()
// Version 3:
def takeTurn() =
  if this.isIntact then
    this.brain match
      case Some(actualBrain) =>
        actualBrain.controlTurn()
// Version 4:
def takeTurn() =
  if this.isIntact then
    this.brain match
      case Some(actualBrain) =>
        actualBrain.controlTurn()
      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-- 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 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) => 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 Options:

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.2, you did the first two parts of the Robots assignment. Now on to the next two.

Here are a 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.

Oh, and there’s one warning sign that I need to put up before you get started:

About the get method on Options

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 forbade its use in O1’s assignments.

That applies to these robot assignments, too. Do not use the get method on Options; use other methods instead. Although get may seem convenient, it’s a bad habit. The method breaks the type safety of the language and is suitable for certain special circumstances only.

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 && possibleWord.get.length > 10 then
  println("found a long word")

That code “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. In O1, it has none at all. The method introduces an element of risk to our programs while achieving nothing of note.

Robots, Part 3 of 9: 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.

    • CompassDir objects have a parameterless clockwise method, which returns the direction that’s 90 degrees clockwise from the original. (E.g., East.clockwise returns South.) Feel free to use it.

  3. Examine RobotBrain’s documentation and Scala code. Notice in particular the methods controlTurn (which you called from RobotBody) and moveBody. You don’t need to make any changes to this trait at this point. Instead, you’ll next implement the Spinbot subtype.

  4. In class Spinbot, the moveBody method doesn’t do anything. Implement the method.

  5. In class RobotWorld, the methods robotWithNextTurn, advanceTurn, and advanceFullRound are missing. Implement them. Use higher-order methods on collections where appropriate.

  6. 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. 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.

  1. In class RobotBody, implement the neighboringSquare method.

  2. In class RobotBrain, implement isStuck, locationInFront, squareInFront, robotInFront, and advanceCarefully.

A+ presents the exercise submission form here.

We’ll continue with the robots in Chapter 9.1.

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 simpler 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 difficult 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 how an Option may 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) then 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 then 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 do
  myCode(value)

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 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 now; you’ll learn something. Can you eliminate all matches?

More practice (and the treacherous get method)

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 on None. As discussed above you should not use 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 that you don’t need this method.

Additional questions in the same style

if x.isDefined then Some(myCode(x.get)) else None
x.isEmpty || myCode(x.get)
x != None && myCode(x.get)
if x.isDefined then
  myCode(x.get)
if x.isDefined then x.get else None
if x.isDefined && myCode(x.get) then x else None
if x.isEmpty || myCode(x.get) then x else None
if x.isDefined then 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 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.

    • Do not use the get method on Options, 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, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen, Juha Sorva, and Jaakko Nakaza. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using O1Library for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services; dozens of Aalto students and others have also contributed.

The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits for this page

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

a drop of ink
Posting submission...