Optional .map() vs. flatMap(): why not always use .flatMap()?

It seems Optional.map() when the transform closure returns nil, then result is Optional(nil) , which is same as Optional.some(nil)? Still a nil but is wrap up an optional? But Optional.flatMap() when the transform closure returns nil, the result is just nil, which is simpler. Why not just always use flatMap, I want to ask?

There must be reason we have both Optional.map() and Optional.flatMap()? Any good explanation?

let anOptional: Int? = 123
let aNilValue: Int? = nil

// Trailing closure in this context is confusable with the body of the statement; pass as a parenthesized argument to silence this warning
let g = anOptional.map {_ in aNilValue }
print("optional.map returns: \(g)")
//prints: optional.map returns: Optional(nil)

let h = anOptional.flatMap {_ in aNilValue }
print("optional.flatMap returns: \(h)")
//prints: optional.flatMap returns: nil

map guarantees that the transformation is a non-optional value iff the origin optional value is non-nil, while flatMap is allowed to return nil. If you return an optional value through map it will be wrapped, while flatMap allows you to flatten the nested generic type hierarchy.

// Int??? aka Optional<Optional<Optional<Int>>>
type(of: Optional<Int>.none.map { _ in Optional<Int>.none }.map { _ in Optional<Int>.none })

// Int? aka Optional<Int>
type(of: Optional<Int>.none.flatMap { _ in Optional<Int>.none }.flatMap { _ in Optional<Int>.none })

It really depends on your needs and type context as there is no true "better or simpler" here.

3 Likes

Following on @DevAndArtist’s point here, the reason to use map is in any circumstance where the closure cannot return nil. In this instance the code is much simpler: no need to check for nil on the way back out, no weird interactions with optional promotion, likely faster compile times.

1 Like

Optional promotion usually confuses me here when dealing with a non-optional return value function

let f: (Int) -> String = { "\($0)" }
let x = Int?.some(10)

print(type(of: x.map(f))
print(type(of: x.flatMap(f))

These both print the same, but if you're unaware of optional promotion on function return types, then you might think that x.flatMap(f) shouldn't compile because of flatMap type signature. But the following compiles as well

let f: (Int) -> String = { "\($0)" }
let g: (Int) -> String? = f

So flatMap is applying (I think) optional promotion to the return type of the function in order to type check. I think a warning in this case would be helpful. Similar to the warning for the deprecated Array.flatMap:

func flatMap<ElementOfResult>(
  _ transform: (Element) throws -> ElementOfResult?
) rethrows -> [ElementOfResult]

Source of Optional map and flatMap:

 @inlinable
  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
    }
  }

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

so map wrap transform result in optional, then return this value, while flatMap don't wrap in optional. But due to automatic promotion to optional, it end up wrapped in optional just the same. The end result seems to be the same. I cannot describe what's the difference even with the source.

I wish there is some simple guideline on when to use optional map vs. flatMap

Consider this code:

func createURL(_ string: String) -> URL? {
    return URL(string: string)
}

let s: String?
let u1 = s.map { createURL($0) }
let u2 = s.flatMap { createURL($0) }

The difference here is revealed in the types of u1 and u2. The type of u1 is URL??, and the type of u2 is URL?. In this instance, using map gives you a double-optional. You will have nil if the original string was nil, and .some(nil) if the string failed to parse as a URL. This is rarely helpful! flatMap allows you to merge the nils together.

This is the guideline: if the closure you pass to map can return an Optional, you almost certainly want flatMap. (Not always, but almost certainly.) This actually generalises: for all types U that have a map/flatMap pair, if the closure you want to pass returns a U then you probably want flatMap.

5 Likes

I find ignoring the implementations and focus on the type signatures helpful. There are also several maps and flatMaps in the standard library and frameworks that may help your understanding. The type signature of most of them is similar and imply some commonalities.

Note: I'm will use Optional<A> for A?, Array<A> for [A] as (I find it) easier to see the similarities.

map

The type signature of Optional<Wrapped>.map is

extension Optional {
  func map<U>(
    _ transform: (Wrapped) -> U
  ) -> Optional<U>
}

Similarly, the type signature of Array<Element>.map is

extension Array {
  func map<U>(
    _ transform: (Element) -> U
  ) -> Array<A>
}

The type signature of Result<Success, Failure>.map is

extension Result {
  func map<U>(
    _ transform: (Success) -> U
  ) -> Result<U, Failure>
}

All three of these methods have have the same structure. They take a function from a type parameter (Wrapped, Element, Success) to a generic (U), and they return a new value of type Optional<U>, Array<U>, Result<U, Failure>. More succinctly

Optional<Wrapped>.map        + ((Wrapped) -> U) -> Optional<U>
Array<Element>.map           + ((Element) -> U) -> Array<U>
Result<Success, Failure>.map + ((Success) -> U) -> Result<U, Failure>

Each of these generic types that looks like F<A>, where F = Optional, F = Array, or F = Result<_, Failure>. And their maps have the following type signature

F<A>.map + ((A) -> B) -> F<B>

That is, you can think of map as preserving the structure of your generic while changing the values in the container.

let x: Optional<Int> = .some(2) // or 2
let y: Optional<Int> = nil
let xs: [1,2,3]
let success: Result<Int, Error> = .success(2)
let failure: Result<Int, Error> = .failure(anError)

x.map { $0 + 1 }       == 3
y.map { $0 + 1 }       == nil
xs.map { $0 + 1 }      == [2,3,4]
success.map { $0 + 1 } == .success(3)
failure.map { $0 + 1 } == .failure(anError)

Notice that the structure didn't change. If I started with .some, map kept the same structure but changed values by incrementing them by 1. Similarly for the other values.

flatMap

We have slightly different type signatures for flatMap:

extension Optional {
  func flatMap<T>(
    _ transform: (Wrapped) -> Optional<T>
  ) -> Optional<T>
}

extension Array {
  func flatMap<T>(
    _ transform: (Element) -> Array<T>
  ) -> Array<T>
}

extension Result {
  func flatMap<T>(
    _ transform: (Success) -> Result<T, Failure>
  ) -> Result<T, Failure>
}

Notice difference in the type signatures. map's closure has type (A) -> B and flatMap's closure has type (A) -> F<B> where F corresponds to Optional, Array or Result<_, Failure>.

Before looking at what flatMap does to particular values, let see what map does

let f: (Int) -> Optional<Int> = {
  $0.isMultiple(of: 2)
    ? .some($0 + 1)
    : nil
}

let x: Optional<Int> = .some(2)
let y: Optional<Int> = .some(3)
let z: Optional<Int> = nil

x.map(f) == .some(.some(3))
y.map(f) == .some(.none)
z.map(f) == .none

let xs: Array<Int> = [1,2,3]
xs.map { Array(repeating: $0, count: $0) } == [[1],[2,2],[3,3,3]]

let g: (Int) -> Result<Int, Error> = {
  $0.isMultiple(of: 2) 
    ? .success($0 + 1)
    : .failure(Error.evenNumbersOnly) 
}
let success2: Result<Int, Error> = .success(2)
let success3: Result<Int, Error> = .success(3)
let failure: Result<Int, Error> = .failure(anError)

success2.map(g) == .success(.success(3))
success3.map(g) == .success(.failure(Error.evenNumbersOnly))
failure.map(g)  == .failure(anError)

What about flatMap?

x.flatMap(f) == .some(3)
y.flatMap(f) == nil
z.flatMap(f) == nil

let xs: Array<Int> = [1,2,3]
xs.flatMap { Array(repeating: $0, count: $0) } == [1,2,2,3,3,3]

success2.flatMap(g) == .success(3)
success3.flatMap(g) == .failure(Error.evenNumbersOnly)
failure.flatMap(g)  == .failure(anError)

So map yielded nested values of type F<F<B>> and flatMap yielded F<B>. In fact, you can think of flatMap as map then join, where join is a function with the following type signatures

func join<U>(_ x: Optional<Optional<U>>)               -> Optional<U>
func join<U>(_ x: Array<Array<U>>)                     -> Array<U>
func join<U>(_ x: Result<Result<U, Failure>, Failure>) -> Result<U, Failure>

That is,

join(optionalValue.map(f)) == optionalValue.flatMap(f)
join(array.map(f))         == array.flatMap(f)
join(result.map(f))        == array.flatMap(f)
4 Likes