Optionals as Collections

Sometimes when working on a thing, I get the sense I’m swimming up stream — that it’s just a little too hard and there has to be a better way.

Sometimes this prompts me to do a little more research and I’ll find the solution staring me in the face. But sometimes I continue to miss the obvious, post about it, and rely on the kindness of smarter people to straighten me out.

In the case of my last post on applying optional values to failable initializers, the role of smarter people is played by Ole Begemann, Will Hains, and a bunch of helpful redditors.

TL:DR;

I re-invented the wheel with applyOptional, which is functionally identical to calling flatMap on an optional type.

And given this, Haskell already has an operator for calling flatMap (a.k.a. bind) on an Optional (a.k.a. Maybe) and it’s >>=. To avoid confusion, we should probably use this if an operator proves necessary.

Further, this all sounds familiar enough that I think I might have known it at one point or another. But I never really understood it, not on an intuitive level. When the time came, binding an optional via flatMap never occurred to me.

So in an effort to make the lesson stick this time, I’ve tried to go back to basics. Thus, the rest of this post.1

Flattened Arrays

First, how on earth does flatMap apply to optionals anyway? Isn’t it supposed to deal with nested arrays or something?

Well, sort of. Let’s start by looking a little more into that nested Array scenario. We can think of a plain ol’ map on an Array like this:

[T].map((T)->U) -> [U]

Which is to say, map is a method on an array of Ts that, given a function that turns a lone T into a U, will return an array of Us.

Now, we can imagine giving our map a function that returns an array of Us instead of a single U, and then we would end up with something like:

[T].map((T)->[U]) -> [[U]]

See how now we’re returned a nested [[U]]? That’s not always super useful. Sometimes we just want [U].

flatMap deals with this by adding an operation that “flattens” the output before returning it. In this case it will unwrap our [[U]] into a simple [U]:

[T].flatMap((T)->[U]) -> [U]

This seems like a useful (if rather niche) utility. But how does it apply to Optionals?

Flattened Optionals

My big mistake was thinking about optionals as just another enum when, in several situations, Swift treats them more like a collection of zero or one elements.

Viewed in this light, the same examples we used above for arrays can be applied to optionals. A map works like this:

T?.map((T)->U) -> U?

That is, given a collection of zero or one Ts and a function that turns a T into a U, map will return a collection of zero or one Us.2

And just as with our array example, we can imagine a scenario where we pass this map a function that returns a collection of zero or one Us instead of a lone U. This would result in:

T?.map((T)->U?) -> U??

Note the Optional<Optional<U>> at the end there. We’d pretty much always want this to be unwrapped or “flattened” a level before we’d use it, and that’s exactly what flatMap on an optional does:3

T?.flatMap((T)->U?) -> U?

Where Have I Seen this Before?

If we think back to the previous post, I was bemoaning how some failable initializers in Foundation such as URL(string:) don’t take optional parameters. But looking at it now, we see the signatures of these existing initializers exactly fit what flatMap expects as a parameter:

//Takes a String, returns a URL?:
URL.init?(string: String)
//Takes a T, returns a U?:
(T)->U?

With no further modification we can pass these initializers into a flapMap on an optional like so:

let urlString: String? = textField.text
let maybeURL = urlString.flatMap(URL.init)

And so it’s now clear to me why URL(string:) doesn’t support optional parameters. It would break compatibility with a crucial part of Swift’s collection-processing toolkit.

But moreover, adding an optional param would be akin to asking URL.init to accept an array of strings. It’s not URL’s responsibility to know which elements of what collections to use to instantiate itself. Instead, it’s our program’s job to whip our collections into the accepted element(s), and pass those on to URL.

And while it’s sometimes difficult to remember Optional can, in this sense, be a collection in need of whipping, it none the less has a full complement of higher order functions (including flatMap) making it up to the challenge, regardless.


1: As the Field Notes folks say, “I’m not writing it down to remember it later, I’m writing it down to remember it now.”↩︎

2: We don’t see this used much in practice because Swift gives us the optional chaining operator as a language feature. Functionally,

let str = maybeURL.map { $0.absoluteString }

and

let str = maybeURL?.absoluteString

are equivalent.↩︎

3: Note this is also the rational for why the recently deceased Sequence.flatMap worked the way it did. If:

[T].map((T)->[U]) -> [[U]]
[T].flatMap((T)->[U]) -> [U]

and

T?.map((T)->U?) -> U??
T?.flatMap((T)->U?) -> U?

then

[T].map((T)->U?) -> [U?]
[T].flatMap((T)->U?) -> [U]

makes a certain amount of sense. Especially if we consider both arrays and optionals to be sequences of zero or more elements.↩︎