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)