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 whileflatMap
removes it and strips outnil
values. flatMap
closures return optionals andnil
values are automatically filtered out.
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.
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:
- The
transform
closure returns a non-optionalU
. - 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.
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??
.
The Closing Brace
Let’s pull it all together:
- When dealing with collections,
map
preserves optionality whileflatMap
removes it and strips outnil
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?
}