Dealing with Swift's Pyramid of Doom


Update, 07 Feb 2017
As of Swift 3, some of the code below may not work properly anymore. To see what changed, read Pyramid of Doom Updated (Swift 3).


Original Post

Today we continue the topic of avoiding Swift’s Pyramid of Doom that we started in previous post, on guard statement.
This time, we cover a feature from Swift 1.2 – it’s not a new thing, but still very handy and useful to have in your arsenal.

Every now and then I see older code that does something like this:

func pyramidOfDoom(x: Int?, y: Int?, z: Int?) {
    if let x = x {
        if let y = y {
            if let z = z {
                //do something with x, y and z
                print("\(x, y, z)")
            }
        }
    }
}

It works and was necessary in Swift 1.0 (unless you added your own workaround for it).

In Swift 1.2, if let allows unwrapping multiple optionals, which lets us rewrite previous example into

func noPyramid(x: Int?, y: Int?, z: Int?) {
    if let x = x, y = y, z = z {
        //do something with x, y and z
        print("\(x, y, z)")
    }
}

So much better!

What’s also useful and helpful for keeping your code clean, is the ability to add where clause to your optional binding.
It lets you check not only if optionals hold any value, but also if that value meets certain condition.

Let’s take a look at code we would write normally, using if statement

func ifStatement(x: Int?, y: Int?, z: Int?) {
    if let x = x, y = y, z = z {
        if (z < 4) {
            //do something with x, y and z
            print("\(x, y, z)")
        }
    }
}

Assuming we need z to be less than 4 - code seems pretty normal. Let’s rewrite it to use where clause

func whereClause(x: Int?, y: Int?, z: Int?) {
    if let x = x, y = y, z = z where z < 4 {
        //do something with x, y and z
        print("\(x, y, z)")
    }
}

One less level of code indentation and a bit cleaner code. You can add one where clause per each let statement

func multipleWhereClause(x: Int?, y: Int?, z: Int?) {
    if let x = x where x < 2,
       let y = y where y < 3,
       let z = z where z < 4 {
            //do something with x, y and z
            print("\(x, y, z)")
    }
}

which acts exactly the same as

func multipleWhereClause2(x: Int?, y: Int?, z: Int?) {
    if let x = x, y = y, z = z where x < 2 && y < 3 && z < 4 {
        //do something with x, y and z
        print("\(x, y, z)")
    }
}

Which version to use? It’s mostly a matter of preference and depends what you think is easier to read.

I prefer version #1, as it’s a bit more explicit and for the same reason I advise you do the same – if you can add more readability by spending few more seconds on typing additional let statements – do so, people reading your code later will be grateful.

One important note to remember, is that assignment and unwrapping happens before the condition in where clause is checked.
This means, if your condition is unrelated to unwrapped variables – it might be better to check the condition first. Why?

func expensiveFunction(startWith: Int) -> Int? {
    //despite what you see here
    //it is really expensive to call this function!
    return startWith * 2
}

func isTodayThursday() -> Bool {
    //I'm writing it on Tuesday, so we can optimize ;)
    return false
}

func expensiveUnwrapping() {
    if let x = expensiveFunction(2), y = expensiveFunction(5) where isTodayThursday() {
        print("\(x, y)")
    }
}

It matters how do we get our optional values. If, as in the example above, they come from a function that is really expensive to call – it would be smart to call it only if we can be sure that returned value will actually be used.

In the example above, our expensive function is being called twice, even though we don’t even get to use x and y variables.

For similar cases, you might use the fact that we are allowed to use one logic statement (outside of where clause) as long as it’s the first clause in the if let statement

func unexpensiveUnwrapping() {
    if isTodayThursday(), let x = expensiveFunction(2), y = expensiveFunction(5) {
        print("\(x, y)")
    }
}

This way our expensive functions will be called only if our initial logic clause is true (according to answer by Chris Lattner on google groups).

Adding one logic clause at the beginning, works exactly the same with guard statement

func guardUnexpensive() {
    guard isTodayThursday(), let x = expensiveFunction(2), y = expensiveFunction(5) else {
        return
    }
    print("\(x, y)")
}

That’s it for now. In one of the next articles I will go into pattern matching and I’ll explain why following statements produce exactly same results. Stay tuned!

if let x = x {
    printSomething()
}

if case let x? = x {
    printSomething()
}

if case .Some(let x) = x {
    printSomething()
}

Related Posts

GitHub Pages and Automatic Deployment

Looking for a free website hosting and automatic deployment after source code changes? Try GitHub pages and Wercker!

Credit Card Validation

Credit Card validation isn't as hard as it looks -- and you can add it to your app in a few minutes only!

Swifty function currying

Function currying is something I wanted to dig into for some time now, and finally I found a good time for it, especially now, after some changes introduced to the syntax in Swift 3.

Pyramid of Doom Updated (Swift 3)

Since last posts about using `let` and `guard`, Swift 3 came out and changed a few things here and there. Let's see what's new!

Let and guard statements in Swift

Swift 2 was announced in June, soon to be a year ago. Still, some of the concepts it introduced are new to many iOS developers.