Reducing Two Option[T]’s in Scala

综合编程 2016-04-16

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:

  1. If both are empty, return None
    .
  2. If A
    has a value and B
    doesn’t, return A
    .
  3. If B
    has a value and A
    doesn’t, return B
    .
  4. If A
    and B
    have a value, return Some(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

责编内容by:Asyncified (源链)。感谢您的支持!

您可能感兴趣的

Spark Query Plans (Gotcha!) I was exploring why Spark was taking hours to run a support vector machine o...
Calculate Option Int’s advance with scala I'm new to scala and I have a listbuffer in a map with this structure : List...
Christopher Georgen at Rethink Trust President and Chief Architect at Topl, Chris leads the design of To...
Implicit, to be or not to be 隐式转换是什么鬼 有时候,一加一等于 11,比如 JavaScript 1+"1" "11" 当两个类...