FlatMap and CompactMap Explained in Swift

Written by Reinder de Vries on July 12 2020 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 tutorial, we’ll discuss map(_:), flatMap(_:) and compactMap(_:).

Here’s what we’ll focus on:

  1. How map(_:) transforms a collection or sequence by applying a closure to it
  2. How flatMap(_:) can flatten an input array, after calling map(_:)
  3. How compactMap(_:) removes nil from the input array

In a previous tutorial, 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.

Ready? Let’s go.

  1. Using the Map Function in Swift
  2. Using the FlatMap Function
  3. Using the CompactMap Function
  4. Why Use FlatMap and CompactMap?
  5. Further Reading

Not yet familiar with closures or higher-order functions? Make sure to read up on those first:

Using the Map Function in Swift

As a quick refresher on higher-order functions in Swift, our starting point is the map(_:) function. This function applies a transformation to each of the elements in a sequence, like an array or dictionary.

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.

We’re calculating the square of every number in numbers. 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.

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 tutorial. Let’s first move on to learn about flatMap(_:) and compactMap(_:).

Get hired as an iOS developer

Learn to build iOS 13 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

Using the FlatMap Function

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. No transformation or operation is happening. When you call flatMap(_:) on the numbers array though, instead of map(_:), you end up with a flattened array of individual numbers. Unlike the input array numbers, the result array doesn’t contain nested arrays!

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)

See how giraffes contains an array of arrays? In the above code, the function filter(_:) is called on every nested array inside giraffes. We only want integers (giraffes!) that are greater than 10. The resulting arrays are flattened into one “flat” array, and assigned to tallest.

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

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

It’s important to note that 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.

Using the CompactMap Function

The compactMap(_:) function removes nil values from the input array. It’s super useful when working with optionals.

Before Swift 4.1, the flatMap(_:) function (above) 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 is failable: it can return nil – 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]

The compactMap(_:) function automatically removes nil elements from the returned array, after calling map(_:) on it. 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 FlatMap and CompactMap?

Before we discuss the real-world use cases of flatmap and compactmap, let’s do a quick recap of these higher-order functions and their purposes.

  1. The map(_:) function applies a closure to an input collection, and returns the transformed collection
  2. The flatMap(_:) function does the same, and it also flattens the resulting collection
  3. The compactMap(_:) function does the same as map(_:), and it also removes nil from the resulting collection

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.

You could, in a music app for example, have 3 arrays: songs, artists and playlists. You combine them in one array, call flatMap(_:) on it, select the songs, artists and playlists for which isFavorite is true, and end up with one flat list of items that have been favorited.

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. An added benefit is compactMap(_:)‘s non-optional return type; the filter(_:) function, in comparison, would have returned an optional value if you filtered out nil.

You can combine flatMap(_:) and compactMap(_:), and even filter(_:) or reduce(_:_:). 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

Neat!

Get hired as an iOS developer

Learn to build iOS 13 apps with Swift 5

Sign up for my iOS development course, and learn how to start your career as a professional iOS developer.

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 your work 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.

×

Start your iOS career
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy