Scala from the ground up

Option, or a value that may be missing

Some and None, getOrElse for a fallback, map and flatMap for chains of lookups, a match to open one by hand, and a function that says in its type that it may find nothing.

Part 11 of 12

Advanced18 min readBack to Courses

A value that might not be there

Some questions have no answer: a name that is not in the map, a row that does not exist, a field left blank. In Scala the answer is Option[A]: Some(value) when there is something, and None when there is not. The two are the only shapes, so a reader can see that the empty case was considered.

This takes the place of the null you may know from other languages. A null is silent — it travels through a program and fails later, somewhere that has nothing to do with the cause. An Option makes the caller deal with the missing case at the point where the value is used, and the return type says so before you read the body.

getOrElse, map and flatMap

getOrElse(fallback) turns the question into a plain value: the contents when there is one and your fallback when there is not. map runs a function only when there is something inside, and returns an Option either way — Some(46) becomes Some(56), and None stays None without the function running at all.

flatMap is for chaining two questions that each might fail. If the first answers None the second is never asked, and the result is one Option rather than an Option holding another one. A lookup that needs a second lookup is the shape flatMap was made for.

Opening one by hand, and answering one

match is how you read an Option when the two cases need different work: case Some(n) => ... and case None => .... Those two cases are exhaustive, so the compiler is satisfied and the reader can see that nothing was forgotten. It is the longest way to open an Option and the clearest.

A function that may find nothing should say so in its return type: def ageOf(name: String): Option[Int] = people.get(name). get on a map already answers an Option, which is why the body needs no work at all, and the caller is told at the signature that a missing name is normal rather than an error.

Option is a container of zero or one

Option[A] has exactly two shapes: Some(value) holds one value, and None holds none. It is an ordinary value, so it can be stored, passed to a function and returned from one, and the type tells the caller that absence is possible. Some(0) is not empty: zero is a value, and it is different from None.

map changes the contents and leaves the shape alone: a function runs on a Some and a None passes through untouched. flatMap is for a function that itself returns an Option; it joins the two questions into one instead of leaving an Option inside an Option. filter turns a Some into None when the test fails, and getOrElse supplies a fallback only when there is nothing inside.

Matching on Some(n) and None is exhaustive because those are the only two cases. That is the longest way to open one and the clearest when the two answers need different work. A function that may find nothing should say Option in its return type, so the caller is warned before reading the body.

Two lookups in a row

Two lookups in a row are often written as a for expression: for { a <- ageOf(name); b <- cityOf(name) } yield a + b. A line that answers None stops the whole expression and the result is None; if both answer, the yield combines them. It is flatMap and map written in the order the questions are asked.

The same shape works for any number of steps, which is why it is the usual way to read a record whose fields may each be missing.

Three ways to lose the benefit of an Option: calling .get on a None (it throws), wrapping a null in Some(null) (the container now holds the thing you were avoiding), and using map for a function that already returns an Option, which produces Some(Some(x)). Use flatMap for that last case.

Try these next

  • Write def firstEven(nums: List[Int]): Option[Int] and return the first even number, or None.
  • Chain two lookups with flatMap, one that finds a user and one that finds the city for that user.
  • Open an Option with match and print a different line for Some and None.

A lookup, a fallback and a chain

val people = Map("ada" -> 36)

def ageOf(name: String): Option[Int] = people.get(name)

println(ageOf("ada"))                 // Some(36)
println(ageOf("zed"))                 // None
println(ageOf("zed").getOrElse(0))    // 0
println(ageOf("ada").map(n => n + 10)) // Some(46)

def cityOf(name: String): Option[String] =
  if (name == "ada") Some("London") else None

val label = ageOf("ada").flatMap { n =>
  cityOf("ada").map(c => c + " " + n)
}
println(label)                        // Some(London 36)

ageOf("zed") match {
  case Some(n) => println("age " + n)
  case None => println("no age")
}

Looking someone up

Run it and compare the two lookups at every step: one finds a value and the other finds none. Then change the name passed to `ageOf`, and see which lines change and which stay the same.

Output
Click "Run" to execute your code

A safe division, then another step

Run it and compare the successful chain with the two that stop early. Then make `divide` accept a divisor of zero by returning `Some(0)` and see which later lines change.

Output
Click "Run" to execute your code

Prefer getOrElse or match to calling get on an Option. get throws on None, which throws away the whole point of the type, and it is never shorter than the fallback form.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What are the two values an Option[Int] can have?