Blog

The power of map and flatMap of Swift optionals

01 Nov, 2015
Xebia Background Header Wave

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.

Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts