FlatMap And CompactMap Explained In Swift

Written by Reinder de Vries on December 26 2018 in App Development

FlatMap And CompactMap Explained In Swift

Swift has a bunch of functions that are useful for transforming collections and sequences. In this article, we’ll discuss map(_:), flatMap(_:) and compactMap(_:).

In a previous article, we’ve discussed how you can use filter(_:) and reduce(_:). These higher-order functions are super helpful for transforming collections, in a concise and insightful way.

If you’re not familiar with closures, make sure to read up on them first: The Ultimate Guide To Closures In Swift.

Ready? Let’s go.

  1. How To Use map(_:)
  2. How To Use flatMap(_:)
  3. How To Use compactMap(_:)
  4. Why Use These Higher-Order Functions?
  5. Further Reading

This article is written for Swift 4.1+

How To Use map(_:)

Our starting point is the map(_:) function. This function applies a transformation to each of the elements in a sequence. Here’s an example:

let numbers = [2, 3, 4, 5]
let result = numbers.map({ $0 * $0 })

print(result)

Let’s break that down:

First, we’re creating an array numbers with a few integer values. Then, the function map(_:) is called on numbers and its result is assigned to result.

The map(_:) function has one parameter, a closure, which returns the result of $0 * $0. The $0 corresponds to the first parameter of the closure, i.e. the number from numbers that’s being transformed.

In essence, the operation $0 * $0 is called on every number in numbers, and the resulting array is assigned to result. You’re transforming – or “mapping” – one array into another. You’re calculating the square of every number.

Transforming an array with map(_:) is similar to using a for loop, but much more concise. Like this:

let numbers = [2, 3, 4, 5]
var result = [Int]()

for number in numbers {
    result += [number * number]
}

print(result)

Here’s another way to look at it. With map(_:), the input array of numbers is transformed into another array of numbers. Like this:

2 => 2 * 2 => 4
3 => 3 * 3 => 9
4 => 4 * 4 => 16
5 => 5 * 5 => 25

Functions like map(_:) are called higher-order functions, because they take a function as input – as opposed to ordinary values. Higher-order functions can also output functions, which is useful for a programming paradigm called functional programming.

Technically, you can call higher-order functions like map(_:) on any sequence. This includes collections like arrays, dictionaries, and sets, ranges like 1...100 and so-called iterators. Anything that looks like a “list” of values, basically.

We’ll discuss why higher-order functions are useful at the end of this article. Let’s first move on to learn about flatMap(_:) and compactMap(_:).

Become a professional  iOS developer

Get started with iOS 12 and Swift 4

Sign up for our iOS development course Zero to App Store to learn iOS development with Swift 4, and start with your professional iOS career.

How To Use flatMap(_:)

The flatMap(_:) function is similar to map(_:) except that it “flattens” the resulting array. Here’s an example:

let numbers = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let result = numbers.flatMap({ $0 })

print(result)

The above code starts out with a nested array of integers. The numbers array consists of an array of 3 arrays, that each contains 3 numbers.

The closure { $0 } simply returns the first argument of the closure, i.e. the individual nested arrays. When you call flatMap(_:) on the numbers array, you end up with a flattened array of individual numbers.

Let’s look at another example. Imagine you’re working with 4 groups of giraffes, and want to create one single group of giraffes that are taller than a certain height. Here’s how you do that:

let giraffes = [[5, 6, 9], [11, 2, 13, 20], [1, 13, 7, 8, 2]]
let tallest = giraffes.flatMap({ $0.filter({ $0 > 10 }) })

print(tallest)

In the above code, the function filter(_:) is called on every nested array inside giraffes. We only want integers that are greater than 10. The resulting arrays are flattened, and assigned to tallest.

Consider what would happen if we had used map(_:) instead of flatMap(_:). The resulting array wouldn’t be flattened, like this:

[[], [11, 13, 20], [13]]

The flatMap(_:) function calls map(_:) on the array items first, and then flattens it. That’s why something like the following doesn’t work:

let numbers = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let result = numbers.flatMap({ $0 * 2 })

In the above code, the $0 refers to the arrays inside numbers. Multiplying an array by two is impossible, so that’s why this code doesn’t work.

It’s smart to think about flatmapping as seeing an array in one less dimension. You start with a two-dimensional array, and end up with a one-dimensional array after flatMap(_:).

What about using flatMap(_:) with optionals? Let’s look at that next.

The name “compact map” is based on the idea that removing nil items from an array makes the array more compact. Likewise, the name “flat map” comes from flattening the array. And “mapping” is a concept from mathematics, where you associate the values in one set with another set.

How To Use compactMap(_:)

Before Swift 4.1, flatMap(_:) could also be used to filter out nil values from flattened arrays. Since Swift 4.1+, you now use the explicit compactMap(_:) for this purpose.

Here’s an example:

let numbers = ["5", "42", "nine", "100", "Bob"]
let result = numbers.compactMap({ Int($0) })

print(result)

See what happens? The most important part of the code is Int($0). This takes an individual string from numbers with $0 and attempts to convert to an integer, with the Int() initializer.

This Int() initializer can return nil – it’s an optional – so its return type is Int?. As a result, the return type of the mapping transformation is [Int?] – an array of optional integers.

[Optional(5), Optional(42), nil, Optional(100), nil]

However, compactMap(_:) automatically removes nil elements from the returned array. And as such, it’s return type is non-optional.

[5, 42, 100]

In the above code, the type of result is [Int]. If you would have used map(_:), the return type would have been [Int?]. And you would have needed an extra step to unwrap the values from the array, to work with them.

Why Use These Higher-Order Functions?

Working with map(_:), flatMap(_:) and compactMap(_:) in the abstract makes it sometimes hard to imagine their practical use cases. Let’s discuss why you’d want to use them.

Using functions like map(_:) to apply transformations to sequences has a few advantages:

  • It’s more concise than using a for loop, because you don’t need temporary variables and a multi-line for in { } block.
  • You can typically write a call to map(_:) on one line, which (usually) makes your code more readable.
  • Functions like map(_:) can be chained, so you can apply multiple transformations to a sequence one by one.

In general, higher-order functions are useful because they let you apply a function to a sequence of values. Instead of coding the transformation procedurally, you can just apply the function and get a result back.

The most practical use case for flatMap(_:) is working with input values that are grouped, or nested, but the output value you want needs to be one-dimensional.

A practical use case for compactMap(_:) is working with a transformation that can return nil. You save yourself a few trivial steps by letting compactMap(_:) filter out nil values immediately.

Imagine you’re building a social media app. You want to construct a timeline of posts for a user. You use 3 queries to select post IDs for the user, for instance from follower posts, advertisements, and trending topics.

  • You can use map(_:) to expand these IDs into actual Post objects
  • You can use flatMap(_:) to flatten the 3 groups into one collection
  • You can use compactMap(_:) to discard posts that couldn’t be expanded

Become a professional  iOS developer

Get started with iOS 12 and Swift 4

Sign up for our iOS development course Zero to App Store to learn iOS development with Swift 4, and start with your professional iOS career.

Further Reading

It’s worthwhile to learn about map(_:), flatMap(_:) and compactMap(_:), because they make your code more concise and readable. You can add more functional programming to your app’s code. Once you get used to them, you can’t believe you could do without them.

Especially the differences between map(_:), flatMap(_:) and compactMap(_:) are worth pointing out. The first one applies a transformation to a sequence, the second one flattens the resulting array, and the third one removes nil values before returning its result. Awesome!

Want to learn more? Check out these resources:

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.