Blog

The power of map and flatMap of Swift optionals

01 Nov, 2015

Until recently, I always felt like I was missing something in Swift. Something that makes working with optionals a lot easier. And just a short while ago I found out that the thing I was missing does already exist. I’m talking about the map and flatMap functions of Swift optionals (not the Array map function). Perhaps it’s because they’re not mentioned in the optionals sections of the Swift guide and because I haven’t seen it in any other samples or tutorials. And after asking around I found out some of my fellow Swift programmers also didn’t know about it. Since I find it an amazing Swift feature that makes your Swift code often a lot more elegant I’d like to share my experiences with it.
If you didn’t know about the map and flatMap functions either you should keep on reading. If you did already know about it, I hope to show some good, real and useful samples of it’s usage that perhaps you didn’t think about yet.

What do map and flatMap do?

Let me first give you a brief example of what the functions do. If you’re already familiar with this, feel free to skip ahead to the examples.
The map function transforms an optional into another type in case it’s not nil, and otherwise it just returns nil. It does this by taking a closure as parameter. Here is a very basic example that you can try in a Swift Playground:
[code language=”obj-c”]
var value: Int? = 2
var newValue = value.map { $0 * 2 }
// newValue is now Optional(4)
value = nil
newValue = value.map { $0 * 2 }
// newValue is now nil
[/code]
At first, this might look odd because we’re calling a function on an optional. And don’t we always have to unwrap it first? In this case not. That’s because the map function is a function of the Optional type and not of the type that is wrapped by the Optional.
The flatMap is pretty much the same as map, except that the return value of the closure in map is not allowed to return nil, while the closure of flatMap can return nil. Let’s see another basic example:
[code language=”obj-c”]
var value: Double? = 10
var newValue: Double? = value.flatMap { v in
if v < 5.0 {
return nil
}
return v / 5.0
}
// newValue is now Optional(2)
newValue = newValue.flatMap { v in
if v < 5.0 {
return nil
}
return v / 5.0
}
// now it’s nil
[/code]
If we would try to use map instead of flatMap in this case, it would not compile.

When to use it?

In many cases where you use a ternary operator to check if an optional is not nil, and then return some value if it’s not nil and otherwise return nil, it’s probably better to use one of the map functions. If you recognise the following pattern, you might want to go through your code and make some changes:
[code language=”obj-c”]
var value: Int? = 10
var newValue: Int? = value != nil ? value! + 10 : nil
// or the other way around:
var otherValue: Int? = value == nil ? nil : value! + 10
[/code]
The force unwrapping should already indicate that something is not quite right. So instead use the map function shown previously.
To avoid the force unwrapping, you might have used a simple if let or guard statement instead:
[code language=”obj-c”]
func addTen(value: Int?) -> Int? {
if let value = value {
return value + 10
}
return nil
}
func addTwenty(value: Int?) -> Int? {
guard let value = value else {
return nil
}
return value + 20
}
[/code]
This still does exactly the same as the ternary operator and thus is better written with a map function.

Useful real examples of using the map functions

Now let’s see some real examples of when you can use the map functions in a smart way that you might not immediately think of. You get the most out of it when you can immediately pass in an existing function that takes the type wrapped by the optional as it’s only parameter. In all of the examples below I will first show it without a map function and then again rewritten with a map function.

Date formatting

Without map:
[code language=”obj-c”]
var date: NSDate? = …
var formatted: String? = date == nil ? nil : NSDateFormatter().stringFromDate(date!)
[/code]
With map:
[code language=”obj-c”]
var date: NSDate? = …
var formatted: date.map(NSDateFormatter().stringFromDate)
[/code]

Segue from cell in UITableView

Without map:
[code language=”obj-c”]
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let cell = sender as? UITableViewCell, let indexPath = tableView.indexPathForCell(cell) {
(segue.destinationViewController as! MyViewController).item = items[indexPath.row]
}
}
[/code]
With map:
[code language=”obj-c”]
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let indexPath = (sender as? UITableViewCell).flatMap(tableView.indexPathForCell) {
(segue.destinationViewController as! MyViewController).item = items[indexPath.row]
}
}
[/code]

Values in String literals

Without map:
[code language=”obj-c”]
func ageToString(age: Int?) -> String {
return age == nil ? "Unknown age" : "She is (age!) years old"
}
[/code]
With map:
[code language=”obj-c”]
func ageToString(age: Int?) -> String {
return age.map { "She is ($0) years old" } ?? "Unknown age"
}
[/code]
(Please note that in the above examples there need to be backslashes before (age!) and ($0) but unfortunately 🙁 that breaks the formatting of WordPress in this post)

Localized Strings

Without map:
[code language=”obj-c”]
let label = UILabel()
func updateLabel(value: String?) {
if let value = value {
label.text = String.localizedStringWithFormat(
NSLocalizedString("value %@", comment: ""), value)
} else {
label.text = nil
}
}
[/code]
With map:
[code language=”obj-c”]
let label = UILabel()
func updateLabel(value: String?) {
label.text = value.map {
String.localizedStringWithFormat(NSLocalizedString("value %@", comment: ""), $0)
}
}
[/code]

Enum with rawValue from optional with default

Without map:
[code language=”obj-c”]
enum State: String {
case Default = ""
case Cancelled = "CANCELLED"
static func parseState(state: String?) -> State {
guard let state = state else {
return .Default
}
return State(rawValue: state) ?? .Default
}
}
[/code]
With map:
[code language=”obj-c”]
enum State: String {
case Default = ""
case Cancelled = "CANCELLED"
static func parseState(state: String?) -> State {
return state.flatMap(State.init) ?? .Default
}
}
[/code]

Find item in Array

With Item like:
[code language=”obj-c”]
struct Item {
let identifier: String
let value: String
}
let items: [Item]
[/code]
Without map:
[code language=”obj-c”]
func find(identifier: String) -> Item? {
if let index = items.indexOf({$0.identifier == identifier}) {
return items[index]
}
return nil
}
[/code]
With map:
[code language=”obj-c”]
func find(identifier: String) -> Item? {
return items.indexOf({$0.identifier == identifier}).map({items[$0]})
}
[/code]

Constructing objects with json like dictionaries

With a struct (or class) like:
[code language=”obj-c”]
struct Person {
let firstName: String
let lastName: String
init?(json: [String: AnyObject]) {
if let firstName = json["firstName"] as? String, let lastName = json["lastName"] as? String {
self.firstName = firstName
self.lastName = lastName
return
}
return nil
}
}
[/code]
Without map:
[code language=”obj-c”]
func createPerson(json: [String: AnyObject]) -> Person? {
if let personJson = json["person"] as? [String: AnyObject] {
return Person(json: personJson)
}
return nil
}
[/code]
With map:
[code language=”obj-c”]
func createPerson(json: [String: AnyObject]) -> Person? {
return (json["person"] as? [String: AnyObject]).flatMap(Person.init)
}
[/code]

Conslusion

The map and flatMap functions can be incredibly powerful and make your code more elegant. Hopefully with these examples you’ll be able to spot when situations where it will really benefit your code when you use them.
Please let me know in the comments if you have similar smart examples of map and flatMap usages and I will add them to the list.

guest
6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Tikitu
Tikitu
6 years ago

Another useful flatMap trick: if you need to map a function over a list, but the function produces Optionals and you want to skip the nils.
func lookUpUser(username: String) -> User? { … }
usernames.map(lookUpUser) // produces [User?] that might contain nils, users are optional-wrapped
usernames.flatMap(lookUpUser) // produces [User] skipping nils, optionals are unwrapped

Andrew Phillips
Andrew Phillips
6 years ago

> The flatMap is pretty much the same as map, except that the return value of the closure in map is not allowed to return nil, while the closure of flatMap can return nil.
Just to clarify: this is another way of saying that the return type of the function/closure in flatMap is another Optional, whereas the return type of the function/closure passed to map is a “plain” value, not an Optional?
I.e. it’s not so much the case that nil is a “magic value” of some sort, just that by returning nil you’re actually returning a different type.

John
John
6 years ago

In the first part of your cogent post, your sample code:
var value: Int? = 2
var newValue = value.map { $0 * 2 }
// newValue is now 4
is slightly misleading. newValue is Optional(4).
In addition this code snippet:
var value: Int? = 10
var newValue = value != nil ? value! + 10 : nil
// or the other way around:
var otherValue = value == nil ? nil : value! + 1
does not compile. The type must be explicit (: Int?) because the compiler cannot infer the type. (giving me a mismatching types error)

Daniil
Daniil
6 years ago

flatMap is function that should be implemented for every single monad (Optional is not exception). Map is function that belongs to funcors.
In this case flatMap works the way you can see. Also, monadic core of optionals provides us monadful bind chaining (a?.b?.c returns nil if a, or b, or c is nil and Some(c) otherwise). Actualy syntax of optional chaining is sugar, this code compiles in a.flatMap{ $0.b.flatMap…}.
So, I think that ‘if-let’ syntax more prefered than using flatMap and map, at least because it does not require deep understanding of monads theory and also the way easily to read

nspangbo
nspangbo
2 years ago

var formatted: date.map(NSDateFormatter().stringFromDate) —–> var formatted: date.map(NSDateFormatter().stringFromDate($0))

Explore related posts