Three common functional methods explained.
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) }
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
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.
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.
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.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:
nil
.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) }
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.
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.