[swift unboxed]

Safely unboxing the Swift language & standard library


The Strange Case Of Mapping Over Optionals

Map and flatMap are usually collection operations — why would you use them with optionals?

5 September 2017 ∙ Swift Language ∙ written by

When explaining optionals to people, I’ll often compare them to a box. An optional Int isn’t an Int — it’s a box that could have an integer inside, but it could just as well be empty.

In that sense, optionals are like containers that are either empty or hold one thing.

But if they’re containers, are they also collections? Sometimes you’ll see code using map and flatMap on optionals, as if they’re collections — what’s going on there? Aren’t they just single values? 🤔

Traditional Map & Flatmap

You can read more about map elsewhere on this site. Let’s get right to an example:

let numbers = [1, 2, 3, 5, 7, 12, 17, 29]

let squares = numbers.map { number in
  return number * number
}

// squares = [1, 4, 9, 25, 49, 144, 289, 841]

It’s a pretty standard map over some integers that returns an array of integers, with the square of each value.

flatMap is like map but will flatten nested arrays too:

let numbers = [[1, 2, 3], [5, 7, 12, 17, 29]]

let flattened: [Int] = numbers.flatMap { number in
  return number
}

// flattened = [1, 2, 3, 5, 7, 12, 17, 29]

Optionals Inside Arrays

Getting back to optionals, if you have a collection of optionals you’ll get some special behavior:

let numbers: [Int?] = [2, 1, 3, nil, 4, 7, nil, 11]

let mappedNumbers = numbers.map { $0 }

let flatMappedNumbers = numbers.flatMap { $0 }

Quiz time: what do you think are the contents of mappedNumbers and flatMappedNumbers? And what are their types?

Answer: mappedNumbers is an exact copy of numbers. It’s an array [Int?] with eight elements including the two nil values.

flatMappedNumbers on the other hand is an array [Int] with six values: [2, 1, 3, 4, 7, 11]

That means you can return nil inside your flatMap closures. Say we want to calculate the squares of only positive numbers and skip over the rest:

let numbers = [-2, -1, 0, 1, 2]
let squares = numbers.flatMap { (num: Int) -> Int? in
  if num >= 0 {
    return num * num
  }
  return nil
}

// squares = [0, 1, 4]

Notice that if you return nil from the closure, it doesn’t get added to the output array. In contrast, if you used map you’d get an output array of optional integers like [nil, nil, 0, 1, 4]

To summarize:

  • When dealing with collections, map preserves optionality while flatMap removes it and strips out nil values.
  • flatMap closures return optionals and nil values are automatically filtered out.
Map and flatMap on collections

That’s how things work when dealing with collections containing optionals, but what about our original question: how do things work when treating optional values themselves like collections? 📦

FlatMap Meets nil

Now we can return to the concept of optionals as containers. Let’s have a look at the flatMap method defined in Optional.swift in the standard library:

public enum Optional<Wrapped> {
  // [...]
  public func flatMap<U>(
    _ transform: (Wrapped) throws -> U?
  ) rethrows -> U? {

The transform closure you pass in takes a Wrapped, which is whatever type the optional wraps, and returns an optional U?.

  // still in flatMap()
  switch self {
  case .some(let y):
    return try transform(y)
  case .none:
    return .none
  } // end switch
} // end function

Finally, there’s a simple switch statement. If there’s a value in the optional (the .some case), then return the value from applying the closure. If there’s no value in the optional, return .none aka nil.

If you go way back to the return statement of flatMap, you’ll see it also returns an optional U?. So flat-mapping over a single optional returns an optional.

let singleNumber: Int? = 2
let square = singleNumber.flatMap { (num: Int) -> Int? in
  if num >= 0 {
    return num * num
  }
  return nil
}

In this case, the result square is of type Int? and has the value 4. If singleNumber were a negative number, square would be nil.

The equivalent code without flatMap might look like this:

let singleNumber: Int? = 2
let square: Int?
if let singleNumber = singleNumber, singleNumber >= 0 {
  square = singleNumber * singleNumber
} else {
  square = nil
}

It’s a matter of style here: flatMap offers a more functional-looking approach with a single assignment and the value generated in a single place (the closure) vs. an if with two possible assignments.

To summarize: flatMap over an optional when you might want to return nil, just as you would when using flatMap with a collection.

flatMap on an optional

Map Meets nil

The code to map over an optional looks suspiciously like flatMap:

public func map<U>(
  _ transform: (Wrapped) throws -> U
) rethrows -> U? {
  switch self {
  case .some(let y):
    return .some(try transform(y))
  case .none:
    return .none
  }
}

As with flatMap, map returns an optional U?. The final return type is the same, but there are two big differences in the implementation:

  1. The transform closure returns a non-optional U.
  2. The .some case has to manually wrap your closure’s return value with another .some() to turn it into an optional.

If there’s nothing in the optional, the code drops to the .none case and we get nil just as we did with flatMap.

But if there is a value — your closure has to return something and nil isn’t an option. That’s the big difference between map and flatMap on optionals.

Map on an optional

What happens if you forget, and use map to return an optional? Here’s the same code as before, but with map instead:

let singleNumber: Int? = 2
let square = singleNumber.map { (num: Int) -> Int? in
  if num >= 0 {
    return num * num
  }
  return nil
}

This looks OK if you run it in a playground, but there’s a problem with the final return type: square is a double optional Int??.

Why? The closure returns an optional Int? which gets wrapped in another optional with .Some() and turns into Int??.

YO DAWG I HEARD U LIKED OPTIONALS SO I PUT AN OPTIONAL INSIDE UR OPTIONAL

The Closing Brace

Let’s pull it all together:

  • When dealing with collections, map preserves optionality while flatMap removes it and strips out nil values.
  • Map over an optional when the closure returns a non-optional and nil is never needed.
  • Flat map over an optional when the closure could return nil.
  • Whether you map or flat map over an optional, the return value is always an optional.

(Flat)mapping over single optionals has a more functional feel where you’re applying a function to the value to get a result. If you like that style, I hope you have a better sense of how things work under the hood.

Check out some more examples of using map and flatMap on optionals. Are you using this technique yourself? What are some of your favorite use cases?

}