Trevor Elkins
Published on
/
6 min read

Understanding Optionals

After reading lots and lots of Swift code written by a variety of people, and interviewing candidates with years and years of iOS experience, I've noticed that many smart people still get tripped up by Optional.

To first-time users they may feel like a nuisance and chore to write. The true power isn't initially obvious, and it doesn't help when Xcode tells you to forcibly unwrap everything. Hopefully I can help clear some things up!

Stop Forcibly Unwrapping

Although this should be fairly obvious, I'll put it first since Xcode annoyingly suggests this as a quickfix to many things. Doing so is entirely against the point of optionals. You should triple-check anytime you think you need to do this.

The only exceptions I make are for some Foundation classes, such as Date, when I know for sure it will be fine. Many of these classes have failable initializers because they accept arbitrary input, which is fine to bypass (in my opinion) if you're using it sensibly.

Think Hard

The biggest tip I can give you is that, in the majority of cases, declaring something as Optional is a logical decision you should make. What you choose ends up propagating to other areas of your code, so think hard. What do I mean by this?

When programming, take a step back from your code and ask yourself, "Does it make sense for my value to not exist?" I've found this answer is often no, though of course it depends. Here's a contrived JSON parsing example:

struct Car {
    let make: String
    let model: String

    init?(json: [String: Any]) {
        guard
            let make = json["make"] as? String,
            let model = json["model"] as? String
        else {
            return nil
        }

        self.make = make
        self.model = model
    }
}

Would it make sense for my car not to have a make and model? I don't think so, yet I keep finding code like this in the wild:

// Bad!!
struct Car {
    let make: String?
    let model: String?

    init?(json: [String: Any]) {
        // I was just at the nil dealership the other day...
        self.make = json["make"] as? String
        self.model = json["model"] as? String
    }
}

? vs !

Sometimes you'll run situations where you have a value that's logically never nil when you access it, but due to initialization rules you have to declare it as an optional. You might write some code that looks like:

var someProperty: MyThing?

func doBlah() {
    // If this always exists... is it still optional? :thinking:
    someProperty?.something()
    someProperty?.anotherThing()
    someProperty?.oneMoreThing()
}

What if we changed our example to this instead?

var someProperty: MyThing!

func doBlah() {
    someProperty.something()
    someProperty.anotherThing()
    someProperty.oneMoreThing()
}

This is a textbook case for using an implicitly unwrapped optional. Like I mentioned before, optionals propagate to other areas of your code, sometimes needlessly.

I do want to issue a word of caution to be very careful with this usage. After all, they're optionals under the hood so crashes will happen if you're wrong.

Use Sensible Defaults

The nil-coalescing operator is syntactic sugar for assigning a default value when unwrapping an optional. Only use it when you're setting an actual default value, otherwise you're throwing away all the benefits of optionals.

struct Car {
    let make: String
    let model: String

    init(json: [String: Any]) {
        self.make = (json["make"] as? String) ?? "" // Oh the agony!
        self.model = (json["model"] as? String) ?? "" // Make it stop!
    }
}

Defaulting the properties to empty strings isn't useful for anyone. It doesn't make sense for a car to have a blank make and model. Consumers of this struct will need to know about checking for the empty string case, except now there won't be any type safety. If it walks like an optional and quacks like an optional, just make it an optional.

The Optional Pyramid

A common code smell is nested if/let statements that have a structural dependency on each other. I'm sure you have seen code like:

func pyramid() {
    if let userName = userName() {
        if let password = password() {
            if isPasswordStrongEnough(password) { // Are we there yet?
                logIn(withUsername: userName, password: password)
            } else {
                print("Need a stronger password!")
            }
        } else {
            print("Please enter a password!")
        }
    } else {
        print("Please enter a user name!")
    }
}

Using guard can help clean this up:

func pyramid() {
    guard let userName = userName() else {
        print("Please enter a user name!")
        return
    }

    guard let password = password() else {
        print("Please enter a password!")
        return
    }

    guard isPasswordStrongEnough(password) else {
        print("Need a stronger password!")
        return
    }

    logIn(withUsername: userName, password: password)
}

It's easier to reason about flatter, independent code with fewer branches. The magic here is how guard enforces an early return statement of some kind in its else block, and that variables defined in a guard clause are visible to the surrounding scope.

The Optional Monad

Now that we've covered the basic mistakes, it's time to show you how optionals were meant to be used. The optional type is actually a monad, which is functional programming jargon for a type that has map() and flatMap() functions.

The function prototypes func map<U>((Wrapped) -> U) and func flatMap<U>((Wrapped) -> U?) look scary at first, but really the only difference is that the flatMap() closure returns an optional and the closure for map() does not.

The docs also say:

Evaluates the given closure when this Optional instance is not nil, passing the unwrapped value as a parameter.

With that in mind, you might be used to writing code like:

struct Person {
    let job: Job?
}

struct Job {
    let salary: Int
}

protocol PersonProvider {
    func person(from id: Int) -> Person?
}

func processSalary(provider: PersonProvider, id: Int) -> String? {
    guard
        let person = provider.person(from: id),
        let job = person.job
    else {
        return nil
    }

    return "This person makes \(job.salary)"
}

The guard statement here is actually unnecessary. The same can be written as:

func processSalary(provider: PersonProvider, id: Int) -> String? {
    return provider.person(from: id)
        .flatMap { $0.job }
        .flatMap { "This person makes \($0.salary)" }
}

No branching necessary! If you ever find yourself manually unwrapping a chain of optionals, you probably can use a flatMap() instead.

I also love using flatMap() to reduce if-let statements into a simple one-liner:

if let property = optionalProperty {
    doSomething(with: property)
}

is equivalent to:

optionalProperty.flatMap { doSomething(with: $0) }