Scala for Beginners: How to really use Option (and Try and Either)

Murray Todd Williams
8 min readMar 10, 2021

This is the first of a three-part series of articles, each of which gets you just a little further along in the journey down a really cool rabbit-hole. It is designed for any typical programmer who is picking up Scala as a new language and hasn’t been studying the finer points of Functional Programming. (But wants to learn.) Links to the other articles can be found at the end.

I’ve been programming with Scala for just a little under five years now, and I can still remember what it was like in the beginning. My “gateway drug” was Spark, and writing Spark code was practically a big case of learning by example. That is, I would see a few lines that someone had written and would just mimic and refine and periodically Google something.

Learning new programming languages is nothing new to me—I first learned Basic in the early 80’s and progressed to Pascal and then C and then Java. I’ve dabbled in obscure ones like Clojure, eked-out some necessary scripts in Perl and Python and Tcl and Rexx… the point is that I know that once you learn how to write a looping structure and your basic if/then/else, you can start writing some rudimentary things pretty quickly. And then you read up on some of the idiosyncrasies of each language and you’re off to the races.

And so it was for me with Scala. I read the simple explanations that Scala programs tended to be more resilient because you were supposed to avoid Java’s null and instead leverage “Option” instead. I shrugged my proverbial shoulders and decided it didn’t look too difficult. It was like explicitly stating that a SQL column could be nullable.

Stick figure saying “I think I get the gist of this. It can’t be too hard to use…”

I started using this “Option thing” in any code situations where I knew I might or might not have some piece of data, like this.

case class Person(name: String, 
age: Option[Int],
email: Option[String])

And for the sake of this example, let’s pretend there’s a function…

def sendEmail(address: String): Boolean

… that sends an email and returns true if everything was successful.

I really found myself stumbling and writing clumsy-looking code whenever I tried to use any of these optional fields. If I wanted to access the email, I would first test to make sure there was a value and, if present, I would use the .get operator to fetch it, something like…

// returns true is successful
def invite(guest: Person): Boolean = {
if (guest.email.isEmpty)
false
else
sendEmail(guest.email.get)
}

And although I argued that I tested for the value’s presence before accessing it, I knew instinctively that I wasn’t really supposed to be using get here. Sometimes I would try to use the arguably safer getOrElse method.

println("Guest's email is " + guest.getOrElse("unknown"))

But honestly, it always led me to writing code that would get funky fast.

Confession

I’ve been writing Scala code for five years, and I have to admit that for the first three-and-a-half to four years, I just didn’t think very consciously about how I was handling these Options. And frankly, I never saw much discussion about it in any of the articles that I would read. This is meant to be the blog article I wish I had come across anytime during those first two or three years.

Let me also mention that the same applies for Scala’s Try typeclass. Scala beginners observe pretty quickly that code examples rarely employ the tedious try-catch-finally mechanism that you find all over the place in Java. In a way, it’s nice to be explicit and say “Here I have the experience to know that something could go wrong.” in cases where you’re pulling in potentially unclean data or participating in some network IO operation.

First lesson: pattern matching

If you haven’t taken the time to learn how to write match/case clauses, you should do so immediately. It may seem a little weird at first, and even a little archaic if you think this looks like a switch statement from languages like JavaScript or C (even going back to Fortran!) but it’s not. The earlier example can be rewritten as follows:

def invite(guest: Contact): Boolean = guest.email match {
case Some(e) => sendEmail(e) // returns true if successful
case None => false
}

Voila! No unsafe .getmethod called.

Let’s take a quick look at a few things that are going on here. First of all, this pattern matching process has the benefit that the Scala compiler will warn you if there’s a possible path that you haven’t handled. If I were to drop the case None condition, the compiler will warn me:

scala> def invite(guest: Option[String]): Boolean = guest match {
| case Some(e) => println(e)
| }
^
warning: match may not be exhaustive.
It would fail on the following input: None

The second thing that’s happening is that the Some(e)case clause is unpacking the value from its Option container. It lets me write a partial function based on the String value e rather than using the get method to extract it. It’s worth admitting that my example is a little wonky: I’m actually sending an email (with my pretend sendEmail function) and then returning the value of true.

Second lesson: map

You’re probably already familiar with the .map method when working with collections. For example, if I wanted to convert a list of strings into a list of integers representing the string’s respective lengths, I could write:

scala> List("one","two","three").map(s => s.size)
val res1: List[Int] = List(3, 3, 5)

But I can also use .map on Options! In the same spirit, let’s say I want to count the size of an optional string:

scala> def stringSize(o: Option[String]) = o.map(s => s.size)
def stringSize(o: Option[String]): Option[Int]
scala> stringSize(Some("Hello"))
val res2: Option[Int] = Some(5)
scala> stringSize(None)
val res3: Option[Int] = None

And by the way: if you have a really simple function like s => s.size where you’ve immediately performing an operation on the value, you can use the much faster variation _.size where the underscore is essentially saying “I’m not going to bother naming a variable because I’m going to use it immediately and won’t care about the name after that!” In the initial list map example, I would really write it as:

scala> List("one","two","three").map(_.size)
val res1: List[Int] = List(3, 3, 5)

and in the stringSize function, I would say this instead:

scala> def stringSize(s: Option[String]) = s.map(_.size)

Any beginning Scala programmers should just learn this notation and use it immediately: you’ll find yourself writing so many of these little function snippets (lambdas) that it gets really tedious naming a bunch of function parameters that you’re immediately throwing away!

Let’s look at the sibling typeclass Try

I briefly mentioned earlier the Try typeclass. It’s like Option in that it wraps some other type, and abstracts situations where there may either be a success or a failure. Beginners should note you have to actually import Try from scala.util to use it.

A Try[T] typeclass wraps some type T. The container either resolves to a Success[T] or a Failure, where the Success wraps a value of type T and the Failure wraps some exception. (Strictly, it wraps any Throwable.)

scala> def convert(n: String): Try[Int] = Try(n.toInt)
def convert(n: String): scala.util.Try[Int]
scala> convert("3")
val res6: scala.util.Try[Int] = Success(3)
scala> convert("foo")
val res7: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "foo")

And really quickly: there’s a fast way to convert a Try into an Option. If I want to convert Success(x) into Some(x) and Failure(e) into None, I can just call the convenient .toOption method like this:

scala> convert("5").toOption
val res10: Option[Int] = Some(5)
scala> convert("bar").toOption
val res11: Option[Int] = None

So going back to how to avoid wonky code. Let’s pretend there’s a customer database that lets you lookup an email address via the customer ID. And let’s say that a getEmail(id: String) function would throw an exception if either the id didn’t exist or the database system was somehow down. I used to consider writing logic that would look like this:

val email: Try[String] = getEmail("bob")
val success: Boolean = if (email.isSuccess) {
sendEmail(email.get)
} else {
false
}

But there we have that situation where we’re calling the unsafe .get method and requiring ourselves to test before using it, just like we did for option.

Instead, we’ve got our two choices. We can either use pattern matching like this:

val email: Try[String] = getEmail("bob")
val success = email match {
case Success(addr) => sendEmail(addr) // returns true if success
case Failure(e) => false
}

Or we can use .map like this:

val email: Try[String] = getEmail("bob")
val success = email.map(addr => sendEmail(addr)).toOption

Which can be shortened (if you’re map is just going to call a one-parameter function) to read like this:

val success = email.map(sendEmail).toOption

An actually, we’ve got a little bit of a snag here, because the type of the val success will have one of have possible values:

  1. Some(true) if the sendEmail function returned true indicating successful delivery.
  2. Some(false) if sendEmail ran into some problem
  3. None if the earlier getEmail function failed.

Well, maybe you would consider it a snag because of the inherent complexity, but you might see it as better than the match/case example because with that situation, you had no idea if a false had to do with a failure of getEmail or a failure of sendEmail.

While we’re at it, let’s look at Either

Either is a similar kind of typeclass that is more flexible than Option or Try. Its signature is Either[L,R] where there’s a “left” type L and a “right” type R. This might sounds cool and funky, as it gives you the ability to have an “either or” diad of types—maybe you want to represent data that could be numeric or textual, so you could use Either[Int,String], but that’s not what Either was designed for.

Really, you’re supposed to use Either to represent a success or a failure, much like Option or Try. The interesting thing is that you get to actually design things so you can choose what type will represent your failures. For example, if you have an old system (say the aforementioned getEmail) that returns the integer error codes if you have a failure (maybe 1 means the customer ID wasn’t found and 2 means that you’ve lost network connectivity to the database), then you could use the signature:

def getEmail(custID: String): Either[Int,String]

Where you would get a value of Left(2) if the network was down, or Left(1) if you had the wrong ID or Right("bob@acme.com") if you were able to look up the email.

Or you could use something like Either[Throwable,String] if you wanted to model something just like Try, where instead of Failure(exception) you would get Left(exception). Or my honest preferred convention: I sometimes use Either[String,String] where I pass a simple message or what went wrong like Left("ID not found in database") or Left("Network failure") and then successes would look like Right("bob@acme.com").

The point is that Either can be used in place of Try if you want to have some flexibility in how you represent your problems, rather than having to construct some odd custom exception like Failure(new Exception("ID not found in database").

And guess what: you can also do pattern matching for Either or you can use .map. For the pattern matching:

val email: Left[String] = getEmail("bob")
val status: Either[String, String] = email match {
case Failure(e) => Left("Couldn't get email address")
case Success(e) => {
if (sendEmail(e))
Right(e)
else
Left("Couldn't deliver invite")
}
}

I’m actually not going to give an example for .map right now, because it starts getting messy in a way that reminds me of when I was testing Options with if statements and using .get. I will dive into that in the second part of this series where I start looking at something called “flatMap”.

--

--

Murray Todd Williams

Life-long learner, foodie and wine enthusiast, living in Austin, Texas.