Skip to content
This repository has been archived by the owner on Jun 30, 2021. It is now read-only.

Latest commit

 

History

History
142 lines (115 loc) · 4.2 KB

04 option-flatmap.md

File metadata and controls

142 lines (115 loc) · 4.2 KB

Option and flatMap

Consider the following classes and function (we leave out the implementations of the methods for now). Here the methods getBar and getBaz can potentially return a null and consequentially compute can return a null too.

class Foo {
  def getBar: Bar = ???
}

class Bar {
  def getBaz: Baz = ???
}

class Baz {
  def getText: String = ???
}

def compute(foo: Foo): String = {
  val bar = foo.getBar
  if (bar != null) {
    val baz = bar.getBaz
    if (baz != null) {
      baz.getText
    }
    else null
  }
  else null
}

Just as before we can rewrite this such that getBar and getBaz return an Option instead.

class Foo {
  def getBar: Option[Bar] = ???
}

class Bar {
  def getBaz: Option[Baz] = ???
}

class Baz {
  def getText: String = ???
}

To use these new methods in compute we will first start by defining some helper functions for a better understanding of what is going on. First we define computeBaz which, given a Baz, returns the String from getText.

def computeBaz(baz: Baz): String = {
  baz.getText
}

Next, to compute the String from a Bar, we can use the computeBaz we just defined. However, the Baz we need as input for this function is wrapped in an Option (see Bar.getBaz). We can use map on this Option and in that way get the String (also wrapped in an Option, of course).

def computeBar(bar: Bar): Option[String] = {
  bar.getBaz.map(baz => computeBaz(baz))
}

If we want to get the String from a Foo, we can try to apply the same trick as we did in computeBar. What we end up with, however, is not what we wanted: Option[Option[String]. Now we got two Options nested before we have the String. Both Options denote the fact that the value inside could potentially be a null. Of course we can do with only one Option and therefore we can flatten the Option[Option[String]] into an Option[String] by using flatMap instead of map. This operator first maps a value inside an Option to another Option (this results in an Option[Option[T]]) and then flattens it to end up with an Option[T].

def computeFooWRONG(foo: Foo): Option[Option[String]] = {
  foo.getBar.map(bar => computeBar(bar))
}

def computeFoo(foo: Foo): Option[String] = {
  foo.getBar.flatMap(bar => computeBar(bar))
}

If we now substitute all the pieces together, we get a series of nested higher order functions. Verify for yourself that this is correct!

def compute(foo: Foo): Option[String] = {
  foo.getBar
    .flatMap(bar => bar.getBaz
      .map(baz => baz.getText))
}

This code becomes very hard to read quickly, especially with multiple flatMaps, as shown below!

def compute(fooOpt: Option[Foo]): Option[String] = {
  fooOpt
    .flatMap(foo => foo.getBar
      .flatMap(bar => bar.getBaz
        .map(baz => baz.getText)))
}

One way to make this a little more readable is to chain the operators rather than nest them. Note that with this you cannot access the foo or bar anymore inside the .map(baz => ...). If you don't need them there, it is perfectly fine to chain rather than nest the operators.

def computeChained(fooOpt: Option[Foo]): Option[String] = {
  fooOpt
    .flatMap(foo => foo.getBar)
    .flatMap(bar => bar.getBaz)
    .map(baz => baz.getText)
}

A second (and definitely better) way to make this more readable is to rewrite the expression to a for-comprehension. In the code below, you read these lines as:

  • foo <- fooOpt "foo drawn from fooOpt"
  • bar <- foo.getBar "bar drawn from foo.getBar"
  • etc.
def computeWithForComprehension(fooOpt: Option[Foo]): Option[String] = {
  for {
    foo <- fooOpt
    bar <- foo.getBar
    baz <- bar.getBaz
  } yield baz.getText
}

In all three implementations it holds that if fooOpt is null, the computation will terminate immediately by returning a None (Option.empty). The same holds when bar or baz is null: all following operations will be discarded (also known as 'lazy evaluation') and a None will be returned.