compactMap and optionals
Back to the cook at the conveyor belt. This time gift boxes ride along the belt: some hold something, others are empty. The recipe card tells the cook: open the box, put what is inside on the tray, and throw the empty box away. In the end the tray holds only ingredients, with no boxes and no gaps.
In Swift such boxes appear when a closure returns an optional. As you already know, Int("abc") returns an optional: the text may hold no number. If you turn strings into numbers with map, you get an array of optionals — boxes, some of them empty:
let inputs = ["10", "abc", "25", ""]
print(type(of: inputs.map { Int($0) })) // Array<Optional<Int>>
let numbers = inputs.compactMap { Int($0) }
print(numbers) // [10, 25]
print(numbers.reduce(0, +)) // 35Let's go through it line by line:
map { Int($0) }gives an array of optionals, of type[Int?]. The full name of this type isArray<Optional<Int>>, which is whattype(of:)printed. You cannot add up such elements withreduce(0, +): the compiler demands that each value be unwrapped.compactMapopens each box, throws away the empty ones and keeps the contents."abc"and the empty string held no number, sonumbersis[10, 25]of type[Int].reduceadds up the remaining numbers:35.
How the result differs from map
compactMap calls the closure for each element, like map. But it skips an empty result and unwraps the value from a full one. So its array differs in two ways:
- it can be shorter than the original: the four strings of
inputsgave two numbers; - it holds no optionals: the type of
numbersis[Int], and these numbers can be added up and sorted.
In the name compactMap, the word compact means "packed tight": the gaps are thrown away, and the values stand close together.
Here the belt picture is not exact. In the example, the belt carries plain strings, not boxes. The closure itself makes the box: Int($0) returns an optional, and compactMap opens it right away. The original array inputs stays the same, as it does after map.
Not only numbers from strings
Int(…) is not the only thing that returns an optional. A dictionary lookup gives a box too: the key may be missing. Here is how compactMap collects the prices of an order from a menu and skips the dishes the menu does not have:
let menu = ["tea": 50, "cake": 120]
let order = ["tea", "pizza", "cake"]
print(order.compactMap { menu[$0] }) // [50, 120]
print(order.compactMap { menu[$0] }.reduce(0, +)) // 170Let's go through the example:
menu[$0]looks the dish up in the menu and returns anInt?. The menu has no key"pizza", so the pizza givesnil.compactMapthrows thisnilaway and keeps the prices of the tea and the cake:[50, 120].compactMapreturns a plain array, so you callreduceright on the result, as in a chain offilterandmap. The order costs170.
Throw away or replace
Sometimes the array must keep its length: each input string should get its own number. Then you do not throw the empty box away but replace it with a fallback value through ??. inputs.map { Int($0) ?? 0 } gives [10, 0, 25, 0]: four strings, four numbers.
Interviewers like to ask this: how does compactMap differ from map? The answer:
mapreturns an array of the same length; if the closure returns an optional, you get an array of optionals[Int?];compactMapthrows awaynil, unwraps the other values and returns an array without optionals[Int], sometimes shorter than the original.
Common mistake
The strings were turned into numbers with map and added up right away:
let sum = inputs.map { Int($0) }.reduce(0, +) // ✗
print(sum)error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'The program does not compile: map gave an array [Int?], and + cannot add up closed boxes. Under the error, the compiler suggests ?? or !. With ?? 0 the sum comes out right, but with ! the program crashes on the string "abc". Replace map with compactMap, and reduce adds up only the numbers: 35.
Key points
compactMapcalls the closure for each element, throws awayniland unwraps the other values.mapwith a closure that returns an optional gives an array[Int?], whilecompactMapgives an array[Int]thatreducecan add up.- The array after
compactMapcan be shorter than the original; to keep the length, replace an empty value through??.