13

I have a model, which has some Option fields, which contain another Option fields. For example:

case class First(second: Option[Second], name: Option[String])
case class Second(third: Option[Third], title: Option[String])
case class Third(numberOfSmth: Option[Int])

I'm receiving this data from external JSON's and sometimes this data may contain null's, that was the reason of such model design.

So the question is: what is the best way to get a deepest field?

First.get.second.get.third.get.numberOfSmth.get

Above method looks really ugly and it may cause exception if one of the objects will be None. I was looking in to Scalaz lib, but didn't figure out a better way to do that.

Any ideas?

dbc
  • 104,963
  • 20
  • 228
  • 340
psisoyev
  • 2,118
  • 1
  • 25
  • 35
  • 2
    Just a note but the flatMap woun't work as given below several times. It should be `First.second.flatMap(_.third.flatMap(_.numberOfSmth)).get` and is still might throw and exception – korefn Feb 27 '13 at 12:20
  • Indeed, thanks. Thanks everyone for your answers, I've found what I was looking for. – psisoyev Feb 27 '13 at 12:23

4 Answers4

20

The solution is to use Option.map and Option.flatMap:

First.flatMap(_.second.flatMap(_.third.map(_.numberOfSmth)))

Or the equivalent (see the update at the end of this answer):

First flatMap(_.second) flatMap(_.third) map(_.numberOfSmth)

This returns an Option[Int] (provided that numberOfSmth returns an Int). If any of the options in the call chain is None, the result will be None, otherwise it will be Some(count) where count is the value returned by numberOfSmth.

Of course this can get ugly very fast. For this reason scala supports for comprehensions as a syntactic sugar. The above can be rewritten as:

for { 
  first <- First
  second <- first .second
  third <- second.third
} third.numberOfSmth

Which is arguably nicer (especially if you are not yet used to seeing map/flatMap everywhere, as will certainly be the case after a while using scala), and generates the exact same code under the hood.

For more background, you may check this other question: What is Scala's yield?

UPDATE: Thanks to Ben James for pointing out that flatMap is associative. In other words x flatMap(y flatMap z))) is the same as x flatMap y flatMap z. While the latter is usually not shorter, it has the advantage of avoiding any nesting, which is easier to follow.

Here is some illustration in the REPL (the 4 styles are equivalent, with the first two using flatMap nesting, the other two using flat chains of flatMap):

scala> val l = Some(1,Some(2,Some(3,"aze")))
l: Some[(Int, Some[(Int, Some[(Int, String)])])] = Some((1,Some((2,Some((3,aze))))))
scala> l.flatMap(_._2.flatMap(_._2.map(_._2)))
res22: Option[String] = Some(aze)
scala> l flatMap(_._2 flatMap(_._2 map(_._2)))
res23: Option[String] = Some(aze)
scala> l flatMap(_._2) flatMap(_._2) map(_._2)
res24: Option[String] = Some(aze)
scala> l.flatMap(_._2).flatMap(_._2).map(_._2)
res25: Option[String] = Some(aze)
Community
  • 1
  • 1
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97
  • 3
    You don't need to use ugly nesting: because of the associativity of `flatMap`, `a flatMap (b flatMap c)` is equivalent to `a flatMap b flatMap c` – Ben James Feb 27 '13 at 12:24
  • Thanks, you are very right. I usually nest them because this mimics the actual structure of what is mappped/flatMapped over (which **is** nested, as in this case). But it's true that it is more readable as a flat chain of `flatMap`. – Régis Jean-Gilles Feb 27 '13 at 12:43
  • 1
    Note that not nesting flatMaps has the disadvantage of not being able to use a lambda parameter in nested lambda expressions, so although it works in this case I would not recommend it general. It's also slower and creates more function objects. – Jesper Nordenberg Feb 28 '13 at 09:44
  • I can only agree. As for people that wonder why the nested version is faster and creates fewer function objects (on average), it is because as soon as we encounter a `None`, the chain stops and so we don't get to (needlessly) create the function objects further down the chain. With flat chains of `flatMap`'s, all the `flatMap`s are always executed even past the point we encounter a `None`, so this is wasted processing plus needless creation of function objects (those past the point we encountered a `None`). So this is good to know, even if this should not matter much, most of the time. – Régis Jean-Gilles Feb 28 '13 at 10:21
  • Thanks for REPL example! It's nice to be able to play with it. – gnsb Feb 06 '19 at 20:11
10

There is no need for scalaz:

for { 
  first  <- yourFirst
  second <- f.second
  third  <- second.third
  number <- third.numberOfSmth
} yield number

Alternatively you can use nested flatMaps

om-nom-nom
  • 62,329
  • 13
  • 183
  • 228
4

This can be done by chaining calls to flatMap:

def getN(first: Option[First]): Option[Int] =
  first flatMap (_.second) flatMap (_.third) flatMap (_.numberOfSmth)

You can also do this with a for-comprehension, but it's more verbose as it forces you to name each intermediate value:

def getN(first: Option[First]): Option[Int] =
  for {
    f <- first
    s <- f.second
    t <- s.third
    n <- t.numberOfSmth
  } yield n
Ben James
  • 121,135
  • 26
  • 193
  • 155
1

I think it is an overkill for your problem but just as a general reference:

This nested access problem is addressed by a concept called Lenses. They provide a nice mechanism to access nested data types by simple composition. As introduction you might want to check for instance this SO answer or this tutorial. The question whether it makes sense to use Lenses in your case is whether you also have to perform a lot of updates in you nested option structure (note: update not in the mutable sense, but returning a new modified but immutable instance). Without Lenses this leads to lengthy nested case class copy code. If you do not have to update at all, I would stick to om-nom-nom's suggestion.

Community
  • 1
  • 1
bluenote10
  • 23,414
  • 14
  • 122
  • 178
  • 3
    @Downvoter: Would you mind to explain the downvote? Given the OP has to update this nested structure a lot, I think should be allowed to point the OP to the concept of Lenses as alternative solution? – bluenote10 Feb 27 '13 at 15:23