Last week we introduced the Option
type as a way of representing when a function may or may not return a value.
A Collection of One
Another way to think about an Option
is as a collection that contains no more than one element. Consequently, Option
provides some collection-like semantics, e.g., fold
, map
, exists
, and filter
. This allows us to chain operations together without having to check at each step whether the value is Some
or None
. We can put that off until the end of the sequence of operations and only do the check once. That way, the algorithm is more readable; it’s not cluttered with a bunch of if-else
noise.
Say that you have a User
type:
case class User(id: String, name: String)
Let’s say that your system is a website. In the top, right corner of the site, you want to display the name of the user who is currently logged in. You need a function that returns the user currently signed in:
val authenticatedUser: () => Option[User] = // ...
Now why would this function return an Option
? Well, the user browsing your site may not have logged in at this point. If that’s the case, then the current user is None
. So then, the code to get the name of the current user is this:
val nameOpt = authenticatedUser().map(_.name) // nameOpt: Option[String]
Notice how the type of nameOpt
is Option[String]
. In other words, assuming that authenticatedUser()
returns a Some[User]
, Scala knows that the User.name
property is a String
. Nevertheless it propagates the uncertainty, if you will, along through the map
operation. Any operations you do on an Option
only happen if the Option
is a Some
. Otherwise, Scala happily ignores the operation and continues to propagate the None
.
OK, so now that we have a final result Option
, what do we do with it? That’s where Option.getOrElse()
comes in:
val name = nameOpt getOrElse "Guest" // name: String
If nameOpt
is a Some
, name
is the value in the Some
. If nameOpt
is a None
, then name
is "Guest"
. You can test both cases in the Scala REPL. To see it work in the case of a Some
:
val authenticatedUser: () => Option[User] = () => Some(User("bcollins", "Brad Collins")) val name = authenticatedUser() .map(_.name) // Some("Brad Collins") .getOrElse("Guest") // "Brad Collins" // name: String = Brad Collins
And then to see it with a None
:
val authenticatedUser: () => Option[User] = () => None val name = authenticatedUser() .map(_.name) // None .getOrElse("Guest") // "Guest" // name: String = Guest
Unraveling the Options
Remember how the collections API defines the flatMap
method? For instance, this is the List.flatMap()
method:
// signature slightly modified for readability def flatMap[B](f: (A) ⇒ List[B]): List[B]
It’s like map
, but flatMap
takes a transformation function that takes an element and returns a list of values rather than just a single value. Then instead of returning a list of lists, flatMap
flattens them into a single list.
Option
also has a flatMap
method. It takes a function that returns an Option
and, instead of returning a nested Option
(i.e., an Option[Option[B]]
), it just returns an Option[B]
:
def flatMap[B](f: (A) ⇒ Option[B]): Option[B]
Maybe instead of having an authenticatedUser()
function, your app just has a session
property. To get the name of the current user, you have to walk the property tree from the application to the session to the user and then finally the name. Application.session
is an Option
to indicate that there may be no valid session. Session.user
is an Option
to indicate that there may be no user signed in. Finally, maybe we don’t even force a user to have a name, just a user ID:
case class User(id: String, name: Option[String] = None) case class Session(user: Option[User] = None) case class Application(session: Option[Session] = None)
You walk the property tree like this:
val nameOpt = app.session .flatMap(_.user) .flatMap(_.name) val name = nameOpt getOrElse "Guest"
Now if any property is None
, from session
to user
to name
, nameOpt
is None
, and name
is "Guest"
.