UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

What’s the difference between map(), flatMap() and compactMap()?

Three common functional methods explained.

Paul Hudson       @twostraws

Swift gives us map(), compactMap() and flatMap() methods, but although they might sound similar they do very different things. So, in this article we’ll look at map() vs compactMap() vs flatMap() to help you understand what each one does and when it’s useful.

The word all three methods share is “map”, which in this context means “transform from one thing to another.” So, the map() method lets us write code to double all the numbers in an array:

let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }

That will take each value in the array and run it through our closure, where $0 refers to the number in question. So, it will be 1 2, 2 2, 3 * 2, and so on – map() will take a value out of its container, transform it using the code you specify, then put it back in its container. In this case, that means taking a number out of an array, doubling it, and putting it back in a new array.

It works on any data type, so we could use it to uppercase an array of strings:

let wizards = ["Harry", "Hermione", "Ron"]
let uppercased = wizards.map { $0.uppercased() }

map() is able to return a different type from the one that was originally used. So, this will convert our integer array to a string array:

let numbers = [1, 2, 3, 4, 5]
let strings = numbers.map { String($0) }

Things get a little trickier if we go in the opposite direction – if we try to convert those strings back into integers. This is because strings can contain any value: “1”, “5”, and “500” are all strings that can safely be converted to integers, but “Fish” is not. As a result, converting a string to an integer returns an optional integer.

To see this in action, this code uses map() to convert a string array into an array of optional integers:

let maybeNumbers = strings.map { Int($0) }
Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

compactMap(): transform then unwrap

Working with optionals can be annoying, but compactMap() can make life much easier: it performs a transformation (the “map” part of its name), but then unwraps all the optionals and discards any that are nil.

So, this line of code does the same string to integer conversion, but results in an array of integers rather than an array of optional integers:

let definitelyNumbers = strings.compactMap { Int($0) }

There are lots of places in Swift that return optionals, including try?, as?, and any failable initializer like creating an integer from a string – these are all great candidates for compactMap().

For example, if you have a UIView and want to read out all subviews that are image views, you can write this:

let imageViews = view.subviews.compactMap { $0 as? UIImageView }

Or if you have an array of strings and want to know which ones are valid URLs, you can write this:

let urls = urlStrings.compactMap { URL(string: $0) }

So, again: map() will take a value out of its container, transform it using the code you specify, then put it back in its container. compactMap() does the same thing, but if your transformation returns an optional it will be unwrapped and have any nil values discarded.

Optional map(): transform only if we have a value

If you think about it, optionals are similar to arrays: they are also a container with something inside. When we look inside the optional container (when we unwrap the optional), we either find a value or we find nil.

This means that the map() method also exists on optionals: take the value out of its container (an optional), transform it with a closure we provide, then put it back in the container (another optional). If the optional was empty, map() automatically does nothing – it sends back nil.

To illustrate this, imagine we had a getUser() method that accepts an integer and returns the username with that ID if it exists. If it doesn’t exist it sends back nil, so this method will return an optional string.

We can use map() to read the value that was sent back, and transform it:

let name: String? = getUser(id: 97)
let greeting = name.map { “Hi, \($0)” }
print(greeting ?? “Unknown user”)

So, if name contains a string then map() will take it out of the optional, transform it to be “Hi, ” then the name, put it back into an optional, then send it back to be stored in greeting.

Putting the value back into an optional allows us to keep the “maybe it has a value, maybe it doesn’t” situation going longer so that later code can decide what that means. In this case the print() function will either print a greeting or print “Unknown user” – it gets to decide, rather than us forcing “Unknown user” earlier.

flatMap(): transform then flatten

You’ve now seen map() transforming an array of integers into an array of integers (doubling them), transforming an array of integers into an array of strings, and transforming an array of strings into an array of integers. That last transformation returned optional integers, so we also looked at how compactMap() will perform the same transformation but then unwrap the optionals and discard any nil values.

Then we looked at how map() works on optionals: if it has a value it gets unwrapped, transformed, and rewrapped, but if it is nil then it stays as nil.

Now consider this code:

let number: String? = getUser(id: 97)
let result = number.map { Int($0) }

Walk it through step by step – what would result be after that has run?

  • number is an optional string.
  • map() will take the value out of the optional and transform it.
  • In this case, Int($0) will convert the string to an optional integer because the string might be something non-numeric like “Fish”.
  • map() then puts that optional value back into another optional.

So, when that code runs result won’t be an Int or even an Int? – it will be an Int??, which is an optional optional integer. Broadly speaking, any time you see an optional optional anything something has gone wrong and you should rethink.

To be clear, an optional optional means:

  1. The outer optional might exist and the inner optional might exist.
  2. The outer optional might exist but the inner optional might be nil.
  3. The outer optional might be nil, which means there is no inner optional.

Optional optionals are deeply confusing to work with, but this is where flatMap() comes in: it also performs a transformation (the “map” part of its name), but then flattens what comes back so that “optional optional” just becomes “optional”.

That is, either the whole thing exists or nothing exists – it flattens double optionals down to single optionals. Ultimately we don’t care about whether the outer or inner optional exists, only whether there’s a value inside there or not, which is why flatMap() is so useful.

As a result, this code will set result to be Int? rather than Int??:

let number: String? = getUser(id: 97)
let result = number.flatMap { Int($0) }

Mapping and flat mapping: universal friends

It’s possible to use map() and flatMap() with many other things, not just arrays and optionals, which is why we have a general name for types that allow them: functors and monads.

These names sound awfully grand, but it only takes a few minutes to understand:

Those articles outline the basic rules of functors and monads, and there’s a very good chance you’ll say “those rules are obvious!” Yes, they are obvious, but that doesn’t make them any less important – we can add a method named map() to anything we want, but that doesn’t automatically make it a functor.

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.4/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.