Sponsored Link •
|
Summary
Scala has an Option type that provides a type-safe alternative to using null to represent optional values. In this blog post, I show how to replace nested conditionals involving Option with a for-expression.
Advertisement
|
Scala's Option
type has only two possible subtypes, Some[T]
and None
. If an Option
is a Some
, the optional value does indeed exist. If it is a None
, however, the value does not exist. In other words, None
indicates in Scala what null
often indicates in Java.
Idiomatic Scala APIs use Option
to represent optional values. For example, here's a find
method that looks for a specified character, c
, in a specified string, s
:
def find(c: Char, s: String): Option[Int] = s.indexOf(c) match { case -1 => None case c => Some(c) }
If the string contains the character, find
returns the index of the first occurence of the character wrapped in a Some
.
Otherwise, it returns None
to indicate the character does not exist
in the string.
Here's another example. The
superChar
method returns the character at the specified index, i
, of the string, "supercalifragilisticexpialidotious", wrapped in
a Some
, or None
if i
is greater than or equal to the length of "supercalifragilisticexpialidotious":
def superChar(i: Int): Option[Char] = { val song = "supercalifragilisticexpialidotious" if (i < song.length) Some(song(i)) else None }
In the unlikely event that you want to determine the character in supercalifragilisticexpialidotious at the same index as the first occurrence of a particular character in a particular string, you
could first call find
. If it's result is a Some
, you have
the index of that character in the string. You can then pass that index to
superChar
. If it returns a Some
, then you have the
character in supercalifragilisticexpialidotious that sits at the same index as
your chosen character in your chosen string. Now imagine further you want to find
the index of the first occurrence of this result character in your
original string. You could invoke find
again, this time passing in the result character
and the original string.
Now three things could go wrong here:
The traditional Java-style approach to deal with this situation with a nested if-else construct, as is done in the following percolate1
method. If all goes well, it returns the desired index, wrapped in a Some
. But if any of the three potential problems happen, it returns a None
:
def percolate1(c: Char, s: String) = { // (Not idiomatic Scala) val index = find(c, s) if (index.isDefined) { // isDefined is true for Some, false for None val resultChar = superChar(index.get) // get grabs the value out of the Some if (resultChar.isDefined) { find(resultChar.get, s) } else { None } } else { None } }
A more idiomatic way to accomplish this in Scala is with a nested pattern match. That would look like this:
def percolate2(c: Char, s: String) = { find(c, s) match { case Some(index) => superChar(index) match { case Some(resultChar) => find(resultChar, s) case None => None } case None => None } }
While this is a bit more concise and leaves less room for error than the nested if-else construct, Scala has an even better way to accomplish this. You can use a for-expression, like this:
def percolate3(c: Char, s: String) = for { index <- find(c, s) resultChar <- superChar(index) result <- find(resultChar, s) } yield result
Using any of these approaches, a None
will percolate
out no matter where the failure occurs.
Here are some examples in the Scala interpreter. Given c
and "cats"
, percolate3
will return Some(3)
because c
is at index 0 in cats
, index 0 in supercalifragilisticexpialidotious contains s
, and s
in "cats"
appears at index 3:
scala> percolate3('c', "cats") res0: Option[Int] = Some(3)
The previous example made it all the way through. If any of the three potential problems occurs, a None
will occur at that step and percolate out all the way to the result. Here the first call to find
results in None
, so None
percolates out as the result of percolate3
:
scala> percolate3('d', "cats") res1: Option[Int] = None
Here the initial call to find
returns Some(68)
, but because 68 is greater than the length of supercalifragilisticexpialidotious, the subsequent call to superChar
results in a None
, which percolates out:
scala> percolate3('c', "--------------------------------------------------------------------cats") res2: Option[Int] = None
And here the first call to find
results in Some(2)
, and superChar
results in Some(u)
, but because "cats"
doesn't contain a u
, the second call to find
results in a None
, which is returned:
scala> percolate3('a', "cats") res2: Option[Int] = None
Using a for expression for this can seem non-intuitive because we're used to doing
this kind of thing to iterate over collections.
One way to think of an Option
is as a collection that can contain either
zero or one value--like a special kind of List
that can either be an
empty List
or a List
with only one value in it. The nice thing is you can compose up to as many operations like this as you need and be confident None
s that happen at any step along the way will simply percolate through to the end.
Have an opinion? Readers have already posted 4 comments about this weblog entry. Why not add yours?
If you'd like to be notified whenever Bill Venners adds a new entry to his weblog, subscribe to his RSS feed.
Bill Venners is president of Artima, Inc., publisher of Artima Developer (www.artima.com). He is author of the book, Inside the Java Virtual Machine, a programmer-oriented survey of the Java platform's architecture and internals. His popular columns in JavaWorld magazine covered Java internals, object-oriented design, and Jini. Active in the Jini Community since its inception, Bill led the Jini Community's ServiceUI project, whose ServiceUI API became the de facto standard way to associate user interfaces to Jini services. Bill is also the lead developer and designer of ScalaTest, an open source testing tool for Scala and Java developers, and coauthor with Martin Odersky and Lex Spoon of the book, Programming in Scala. |
Sponsored Links
|