Understanding Optional Chaining

Optionals in Swift are an interesting beast. On the one hand, they’re absolutely essential to dealing with Objective-C and C methods, which can return nil and NULL at will. On the other hand, they’re actually a pretty advanced concept that is tough to drop on unsuspecting developers.

As a bridge between the abstract nature of the Optional type and the well-understood nil semantics of Objective-C, Swift provides optional chaining. Users have to know when they’re dealing with optional types, but other than that acknowledgement, they can use them almost like regular types until unwrapping is needed. To make things even easier, optionals that are known to have a value can be declared as implicitly unwrapped and used as if they weren’t optional at all.

The thing is, with those helpful layers of syntactic sugar, it’s difficult to discern what’s really going on. How does ?. work, exactly? What does ! do? I’m going to look into these issues. Be warned, we will dive deep, and this will take two posts. But it will be a ton of fun.

• • • • •

We need to draw a distinction when thinking about optionals. On the one hand, there is the mechanics of what’s happening. On the other, the syntax involved. Since Swift doesn’t have a macro system, I’m going to focus on the mechanics: things that we can represent in code, even if not with the same syntax. Because at its core, optional chaining isn’t magical—it’s just a method call.

Let’s begin by thinking of ?. as an operator. What are its properties?

So right away, we can write a signature for a similar operator, say |-:

operator infix |- { associativity left }

@infix |-<T,U> (T?, f: T -> U?) -> U?

Notice that we require that f return an optional. This seems like a limitation, but we’ll see later that Swift has a nifty trick up its sleeve to make it a non-issue. Assuming this function exists, then, we should be able to write the following:

public class Demo {
    public let subDemo:SubDemo?
    init(subDemo sDemo:SubDemo? = nil) {
        self.subDemo = sDemo
    }
}

public class SubDemo {
    public let count:Int = 1
}

let aDemo:Demo? = nil
let bDemo:Demo? = Demo()
let cDemo:Demo? = Demo(subDemo: SubDemo())

let aCount = aDemo |- { $0.subDemo } |- { $0.count } // {None}
let bCount = bDemo |- { $0.subDemo } |- { $0.count } // {None}
let cCount = cDemo |- { $0.subDemo } |- { $0.count } // {Some 1}

Clearly |- behaves like a clumsier version of ?.. Now all we need is the implementation.

• • • • •

The thing to remember about Optional is that it’s not some arcane magical type; it’s a simple enum, as illustrated by Jameson Quavez in his reimplementation. And armed with that knowledge, we can implement our function like this:

@infix |-<T,U> (opt:T?, f: T -> U?) -> U? {
    switch opt {
    case .Some(let x):
        return f(x)
    case .None:
        return .None
    }
}

If you throw that code in a playground, you’ll see that it works perfectly with the example above—which is somewhat surprising, because the last closure, the call to count, does not return an optional.

This is a feature so common in Swift that we tend to ignore it: optionals can be created on demand. For instance, we never need to wrap return values in an optional when the signature calls for it; we just return nil or the value—and they magically get wrapped. The same thing is happening here: the result of the closures is implicitly wrapped in an optional because f:T->U? demands it.1

• • • • •

Now, if you’ve been reading along with this blog, you may remember my second post on error handling. And if you do, the definition of |- may look awfully familiar. The type signature isn’t quite the same, but the implementation is almost identical to that of Result.flatMap:

extension Result {
    func flatMap<P>(f:T -> Result<P>) -> Result<P> {
        switch self {
        case .Success(let value):
            return .Success(f(value))
        case .Error(let msg):
            return .Error(msg)
        }
    }
}

And indeed, we can implement flatMap on Optional like so:

extension Optional {
    func flatMap<Z>(f:T->Z?) -> Z? {
        switch self {
        case .Some(let a):
            return f(a)
        case .None: 
            return .None
        }
    }
}

Which in turn allows us to write:

let aCount = aDemo.flatMap { $0.subDemo }.flatMap { $0.count } // {None}
let bCount = bDemo.flatMap { $0.subDemo }.flatMap { $0.count } // {None}
let cCount = cDemo.flatMap { $0.subDemo }.flatMap { $0.count } // {Some 1}

So we have found a different way to do what ?. and |- do—but that’s not all. Since Optional<()> is a valid type, these two expressions do the same thing:

let a:Int? =… // some optional

if let a = a {
    println("\(a)")
}

a.flatMap { println("\($0)") }

That’s right. Deeply embedded in the mechanics of optionals, hidden beneath mounds of syntactic sugar, we find flatMap.

Isn’t that interesting?

• • • •

Not to leave a really juicy mystery hanging (what, you’re not wondering what else this flatMap function does?), but we’re not actually done with optionals. The workings of ?. and if-let are only part of the story. We still have to look at how implicitly unwrapped optionals work, and what that ! symbol does, because they are sources of confusion, made worse by the fact that they’re designed to work as if they didn’t exist. I’ll talk about those next time.

But don’t worry. I’ll get back to flatMap.







1 Be careful, though: this automatic wrapping only happens with closures.↩︎

 
94
Kudos
 
94
Kudos

Now read this

The Ghost of Swift Bugs Future

Update: I wrote this with Xcode 7 β1, and playgrounds crashed a lot at the time. As a result, I gave up on testing all the cases, and a lot of errors creeped into the snippets. They are now corrected, thanks to (among others)... Continue →