A Practical Example of FlatMap
By: . Published: . Categories: swift functional-programming.The Swift standard library introduces some unfamiliar concepts if you’re coming
from Obj-C and Cocoa. map
is one thing, but for some, flatMap
seems
a bridge too far. It’s a question of taste, and of background, if something
comes across as a well-chosen, expressive phrase or if it just seems like
status signaling, high-falutin’ bullshit.
Well, I’m not going to sort that all out, but I did find myself rewriting an
expression using a mix of if let/else
into a flatMap
chain recently, so
I thought I’d share how I rewrote it and why.
If you’re mystified by Optional.flatMap
, read on, and you should have a good
feel for what that does in a couple minutes.
I’m not going to demystify everything:
You still won’t know why it’s called flatMap
.
But then, why do we use +
for addition?
And how do you implement it in terms of a fixed number of bits?
Just because you don’t know a symbol’s etymology or a function’s
implementation, that doesn’t mean you can’t make it do useful work for you. If
you treat flatMap
as an operator written using Roman letters,
you can get good value out of it!
Duck, Duck, Goose
Here’s what some deserialization code looked like to start:
init?(json: JsonApiObject) {
guard let name = json.attributes["name"] as? String
, let initials = json.attributes["initials"] as? String
else { return nil }
self.name = name
self.initials = initials
self.building = json.attributes["building"] as? String
self.office = json.attributes["office"] as? String
self.mailStop = json.attributes["mailStop"] as? String
if let base64 = json.attributes["photoBase64"] as? String
, let data = Data(base64Encoded: base64) {
self.photo = UIImage(data: data)
} else {
self.photo = nil
}
}
Notice how you’re trucking along reading, “OK, we set this field, set that
field, set that other field, and WHAT THE HECK IS THAT.” The if let
bit comes
out of left field, breaks your ability to quickly skim the code, and takes some
puzzling to sort out. It also leads to repeating the assignment in both
branches.
Cleaning This Up
Extract Intention-Revealing Method
To start with, we can take the existing code as-is, yank it out into a helper method, and call that:
self.photo = image(fromBase64: json.attributes["photoBase64"] as? String)
This makes the call site in init?
read fine, but we’ve just moved the ugly
somewhere else.
Take Advantage of Guard
Shifting it into a method dedicated to returning an image does
open up using guard let
to make the unhappy path clear:
func image(fromBase64 string: String?) -> UIImage? {
guard let base64 = string
, let data = Data(base64Encoded: base64)
, let photo = UIImage(data: data) else {
return nil
}
return photo
}
Still Too Noisy!
But that’s no real improvement:
- The return values just restate our return type. They’re noise.
- The reader has to manually notice that we’re threading each
let
-bound name into the computation that’s supposed to produce the next one. - We’re forced to name totally uninteresting intermediate values just so we have a handle to them to feed into the next computation.
All told, that’s a lot of noise for something that’s conceptually simple and that should be eminently skimmable.
A Pipeline with Escape Hatch
The pipeline we have is:
- feed in a string
- transform it into data by decoding it as base64
- transform that into an image by feeding it into
UIImage
- spit out the image
The trick is, if any of these steps fails – that is, if any step spits out
a nil
– we just want to bail out and send back a nil
immediately.
It’s like each step has an escape hatch that short circuits the rest of the
pipeline.
Pipeline with Escape Hatch Is Just FlatMap
Well, that’s exactly the behavior that sequencing all these with
Optional.flatMap
would buy you! Have a look:
func image(fromBase64 string: String?) -> UIImage? {
return string
.flatMap { Data(base64Encoded: $0) }
.flatMap { UIImage(data: $0) }
}
And if you inlined it, it’d still be eminently readable, because it
puts the topic first (“hey, y’all, we’re going to set photo
!"),
which preserves the flow of the code and its skimmability,
and you can quickly skim the pipeline to see how we get that value.
Conclusion
Flatmap very clearly expresses a data transformation pipeline, without extraneous syntax and temporary variables.
We backed into using it in this example for reasons of readability, not for reasons of “I have a hammer! Everything is a nail!”
Sometimes, the new tool really is the right tool.
Appendix: Similar Rewrites
This “assign something depending on something/s else” situation happens a lot. And it can shake out a lot of different ways.
If the expression had been simpler, we could have rewritten it using ?:
to
eliminate the repeated assignment target. This often shows up with code like:
- if haveThing {
- x = thing
- } else {
- x = defaultThing
- }
+ x = haveThing ? thing! : defaultThing
Which, in that common “sub in a default” case, can be further simplified:
- x = haveThing ? thing! : defaultThing
+ x = thing ?? defaultThing
And if nil
is an A-OK default, becomes the wonderfully concise:
- let defaultThing = nil
- x = thing ?? defaultThing
+ x = thing
There’s a similar transform that eliminates guard let
stacks by using
optional-chaining, but that deserves a bit more of an example, I think.