Working with Options
In dynamically typed languages, it is a trivial task to write functions that return something or nothing. In a statically typed language like Scala, something does not have the same type as nothing. It is possible to check if something exists and handle the exception, but this is not exactly a good way to solve that kind of problem. Having an Option
is.
A Clojure example of a function that returns something or nil
:
; clojure
(defn divide
[a b]
(if (> b 0)
(/ a b)))
; 'divide' returns an Integer
(divide 4 2)
;-> 2
; or nil
(divide 1 0)
;-> nil
(defn add
[a b]
(+ a b))
; the returned Integer can be passed to other functions
(add (divide 4 2) 2)
;-> 4
In Scala, when we explicitly state that divide
returns something or null
, something will not be of type Int
.
// scala
def divide(a: Int, b: Int) = if (b > 0) a / b else null
divide(4, 2)
//-> Any = 2
divide(1, 0)
//-> Any = null
This would make it difficult to pass the result of divide
to other functions:
def add(a: Int, b: Int): Int = a + b
add(divide(4, 2), 2)
// error: type mismatch;
// found : Any
// required: Int
Meet Option
The example above is the perfect situation in which Option
would be up for good use. An optional, that sometimes is referred to as “maybe type” because of its implementation in Haskell (Maybe
type), encapsulates the result of an expression that can return something or nothing.
If divide
results in an Int
or nothing, we can say it returns Option[Int]
.
def divide(a: Int, b: Int): Option[Int] = if (b > 0) Some(a / b) else None
divide(1, 0)
//-> None
divide(4, 2)
//-> Some(2)
Using Option
would give us the same problem as Any
. Some
is not Int
, so it cannot be argument to functions that require Int
. However, it is easier to destructure an Option
and it is a more idiomatic way of dealing with the something or nothing problem.
Option
as a collection
The most obvious way to destruct an Option
is to use pattern matching.
divide(4, 2) match {
case Some(value) => value
case _ => -1
}
//-> Int = 2
Using pattern matching is fine, but Option
give us a lot more power, because it can be treated as a collection and, thus, we can use high order functions to manipulate its values.
Considering that all our program does is to get the first element of a List of Integers and find the correspondent value in a List of Strings
// assuming that a List[String] is not empty and
// List[Int], if not empty, has valid indexes
val ls = List("a", "b", "c")
def getElement(li: List[Int]): Option[String] = {
val index = li.headOption
index.map(ls(_))
}
val liA = List(2, 0, 1)
getElement(liA)
//-> Option[String] = Some(c)
val liB = List()
getElement(liB)
//-> Option[String] = None
Understanding the getElement
:
- It uses
headOption
instead ofhead
to get the first element of a list.headOption
returns anOption
and is a safer choice if we are not sure if the list is empty.
List().head
// NoSuchElementException: head of empty list
List().headOption
// Option[Nothing] = None
map
being sent to anOption
shows that we can treatOption
like a collection. In thegetElement
example, for each element isindex
(anOption
), get the given element fromls
(aList[String]
).
Another examples when Option
responds to functions from the collection API:
val lA = List(1, 2, 3).headOption
lA.isEmpty
//-> Boolean = false
val lB = List().headOption
lB.isEmpty
//-> Boolean = true
val listOfOptions = List(Some(1), Some(2), None, Some(3), None)
listOfOptions.flatten
//-> List(1, 2, 3)
Option
also has its own methods. The simplest way to get something or return a default value is to use getOrElse
def getHead(l: List[Int]) = l.headOption.getOrElse(-1)
getHead(List(1, 2, 3))
//-> Int = 1
getHead(List())
//-> Int = -1
Note that the default value if nothing is found is an Integer. If we pass it a String, we would have a return type of Any
:
def getHead(l: List[Int]) = l.headOption.getOrElse("not found")
getHead(List(1, 2, 3))
//-> Any = 1
getHead(List())
//-> Any = not found