Reducing Two Option[T]’s in Scala
Every Scala programmer is familiar with the Option[T]
monad. Option[T]
is a container which either has a value (Some
), or doesn’t have a value (None
). It is extremely useful
with avoiding null checks. When I started programming in Scala not long ago, I immediately fell in-love with the concept of having flat, readable code by using it, instead
of having if (foo != null)
checks everywhere in the codebase. If you’re not familiar with it, check out this introduction by Jan Krag.
I needed to implement a classic reduce (fold) method for any two Option[T]
’s:
Let A
, B
be Option[T]
, let f
be a function (T, T) => T
such that:
- If both are empty, return
None
. - If
A
has a value andB
doesn’t, returnA
. - If
B
has a value andA
doesn’t, returnB
. - If
A
andB
have a value, returnSome(f(A, B))
.
I wanted to show a couple of varying approaches to solve this task at hand.
The imperative approach
If you’re coming from a classic object-oriented programming language, the first option that comes to mind is to use a if-else
control statement:
def reduce[T](a: Option[T], b: Option[T], f: (T, T) => T): Option[T] = {
if (a.isDefined) {
if (b.isDefined){
Some(f(a.get, b.get))
}
else a
}
else b
}
And then you look at it and think “OMG, MY EYES” as they start to bleed.
The pattern matching approach
A more Scalaish/functional approach if you will is to use pattern matching against the two options:
def reduce[T](a: Option[T], b: Option[T], f: (T, T) => T): Option[T] = {
(a, b) match {
case (Some(first), Some(second)) => Some(f(first, second))
case (Some(_), None) => a
case (None, Some(_)) => b
case _ => None
}
}
Being rather new to Scala, this is actually the first approach I went with. But then…
Viewing Option[T]
as a collection
Option[T]
can also be viewed a collection containing 0 or 1 elements. This means it has methods such
as map
,
flatMap
,
filter
,
collect
, etc. Generally, the “idiomatic” approach of using an Option[T]
is through these methods:
scala> val opt = Some(3)
opt: Some[Int] = Some(3)
scala> opt.map(_ + 3)
res1: Option[Int] = Some(6)
scala> opt.filter(_ < 2)
res2: Option[Int] = None
scala> opt.filter(_ > 2)
res3: Option[Int] = Some(3)
scala> val empty: Option[Int] = None
empty: Option[Int] = None
scala> empty.map(_ + 3)
res3: Option[Int] = None
scala> empty.filter(_ < 3)
res4: Option[Int] = None
Scala’s collection framework is rich and comes out of the box with many useful data structures
and methods to operate on them.
Since we’re viewing Option[T]
as a collection, we can take advantage of that to reduce the amount of boilerplate in our code. One of those useful methods
is reduceLeftOption
, which applies a binary operation on every two
elements in the collection, going left to right (hence the left in the method name).
++
is a method defined on the TraversableLike
trait that allows
any two collections to be concatenated, applying the left operand and then the right operand:
scala> val a = List(1)
a: List[Int] = List(1)
scala> val b = List(2)
b: List[Int] = List(2)
scala> val c = a ++ b
c: List[Int] = List(1, 2)
scala> val d = List('a')
d: List[Char] = List(a)
scala> val e = c ++ d
e: List[AnyVal] = List(1, 2, a)
We can take advantage of it and use it to apply two options together:
scala> val first = Some(3)
first: Some[Int] = Some(3)
scala> val second = Some(42)
second: Some[Int] = Some(42)
scala> first ++ second
res21: Iterable[Int] = List(3, 42)
scala> first ++ None
res22: Iterable[Int] = List(3)
scala> None ++ second
res23: Iterable[Int] = List(42)
And then applying reduceLeftOption
on the created sequence, passing f
as the binary operation:
def reduce[T](a: Option[T], b: Option[T], f: (T, T) => T): Option[T] = {
(a ++ b).reduceLeftOption(f)
}
For example:
scala> val a = Some(42)
a: Some[Int] = Some(42)
scala> val b = Some(5)
b: Some[Int] = Some(5)
scala> (a ++ b).reduceLeftOption(math.max)
res3: Option[Int] = Some(42)
scala> (a ++ b).reduceLeftOption(math.min)
res4: Option[Int] = Some(5)
Voila! Just like that we go from 9 lines of code with the imperative approach, to a single line of code. Now, we can have a lengthy argument on what’s more “idiomatic” and non less important - readable. At the end of the day it’s definitely to each his own taste. But, none the less, it’s always good to know what powerful constructs the underlying language exposes!
Appendix
Another question one might ask is, what if we have N Option[T]
’s which we need to reduced? How can generalize this approach?
One way is to flatten the sequence and then apply reduceLeftOption
:
scala> :paste
// Entering paste mode (ctrl-D to finish)
def reduce[T](options: Option[T]*)(f: (T, T) => T) = {
options.flatten.reduceLeftOption(f)
}
reduce(Some(1), Some(1), Some(2), Some(4))(_+_)
// Exiting paste mode, now interpreting.
reduce: [T](options: Option[T]*)(f: (T, T) => T)Option[T]
res0: Option[Int] = Some(8)
Some more alternatives can be found in this StackOverflow answer