Search code examples
scalafunctional-programming

Scala: Chain flatmap vs nested flatmap


I wonder in Scala, is there any difference/preference between these 2 pieces of code.

// common code
  case class Person(maybeAddress: Option[Address])
  case class Address(maybeZipCode: Option[String])

chained flatmap:

  maybePerson
    .flatMap(person => person.maybeAddress)
    .flatMap(address => address.maybeZipCode)

nested flatmap:

  maybePerson
    .flatMap(person => person.maybeAddress.flatMap(address => address.maybeZipCode))

Thanks!


Solution

  • With the example provided there is no much other difference than the style.

    If we look at the scaladoc of Option.flatmap

      /** Returns the result of applying $f to this $option's value if
       * this $option is nonempty.
       *
       * This is equivalent to:
       * {{{
       * option match {
       *   case Some(x) => f(x)
       *   case None    => None
       * }
       * }}}
       */
      @inline final def flatMap[B](f: A => Option[B]): Option[B] =
        if (isEmpty) None else f(this.get)
    

    if we rewrite each example of your question following the pattern matching showed in the scaladoc, for the first case it would be

    // from
    maybePerson
        .flatMap(person => person.maybeAddress)
        .flatMap(address => address.maybeZipCode)
    
    // to
    maybePerson match {
      case Some(person) => person.maybeAddress
      case None         => None
    } match { // this match will always be applied
      case Some(address) => address.maybeZipCode
      case None          => None
    }
    

    for the second one would be

    // from
    maybePerson
        .flatMap(person => 
          person.maybeAddress
            .flatMap(address => 
              address.maybeZipCode
            )
        )
    
    // to
    maybePerson match {
      case Some(person) => 
            // this match will be applied only if maybePerson is Some
            person.maybeAddress match {
              case Some(address) => address.maybeZipCode
              case None          => None
            }
      case None => None
    }
    

    As I detailed before, in the example we are analyzing there is no much difference beside the style.

    Maybe in a more complex scenario, you would have values that must be computed, some of them could be expensive to process or produce a side effect that you should rollback if something fails.


    That being said, from my side I prefer to use for comprehensions

    for {
      person  <- maybePerson
      address <- person.maybeAddress
      zipCode <- address.maybeZipCode
    } yield zipCode
    

    It's a more vertical style and avoids having nested blocks of code, unless you extract the anonymous functions to methods or functions declared in variables like this

    def personToAddress(person: Person): Option[Address] =
      person.maybeAddress
    
    def addressToZipCode(address: Address): Option[String] =
      address.maybeZipCode
    
    // first case
    maybePerson
        .flatMap(mapToAddress)
        .flatMap(mapToZipCode)
    
    // second case
    def addressToZipCode(address: Address) =
      address.maybeZipCode
    
    def personToZipCode(person: Person) =
      person
        .maybeAddress
        .flatMap(addressToZipCode)
    
    maybePerson
      .flatMap(personToZipCode)