Thursday, June 11, 2015

Guard is Good (Swift)

Several good fixes and improvements to Swift were introduced at WWDC15 as a part of Swift2 -- the first major update to the excellent Swift programming language. One of these updates was the new `guard` keyword. It immediately resonated with me because I saw that it fixed some pain points that I have often encountered.

Pyramid of Doom


Anyone who has used Swift is familiar with the `if let` construct. It is an elegant way to unwrap an Optional, and do something with the unwrapped value if it is not nil. This leads to code that looks like this:

    func personFromData(data:NSData?) -> Person? {
        if let data = data {
            let json = JSON(data:data) // SwiftyJSON
            if let firstName = json["firstName"].string {
                if let lastName = json["lastName"].string {
                    return Person(firstName:firstName, lastName:lastName)
                }
            }

        }
        return nil
    }

Even in this small example, you can what has been referred to as the Pyramid of Doom.

Swift 1.2, released earlier this year, added support for unwrapping multiple Optional values at once with `if let`. This looks a bit better:

    func personFromData(data:NSData?) -> Person? {
        if let data = data {
            let json = JSON(data:data)
            if let firstName = json["firstName"].string
                   lastName = json["lastName"].string 
            {
                return Person(firstName:firstName, lastName:lastName)
            }
        }
        return nil
    }

Note that we couldn't completely collapse the pyramid because we have to convert the NSData to a JSON object, and you can't throw that statement in with the other assignments that are unwrapping Optionals. 

One problem with the examples so far is that there is no error handling. While it's nice to unwrap multiple Optionals at once, sometimes you might need unique error handling for each nil Optional. That means we're back to the Pyramid of Doom, except it's even worse now:
        
    func personFromData(data:NSData?) -> Result<Person, String> {
        if let data = data {
            let json = JSON(data:data)
            if let firstName = json["firstName"].string {
                if let lastName = json["lastName"].string {
                    return Result(value:Person(firstName:firstName, lastName:lastName))
                }
                else {
                    return Result(error:"missing lastName in JSON")
                }
            }
            else {
                return Result(error:"missing firstName in JSON")
            }
        }
        else {
            return Result(error:"where data was nil")
        }
    }

This is really ugly. Even in the small example above you can see that it's hard to associate the error handling code in each else clause with the corresponding `if let` that failed because of a nil Optional. The error handling code for each error condition is in the reverse order of the code that checks for the error conditions. 

Guard all the Things


Guards are kind of like `if let`, but inverted. Instead of using `if let` to unwrap an Optional while testing for nil, use `guard let ... else` to unwrap the Optional. If the Optional is nil, the else clause is executed. You have to return from the else clause. Our example can now be rewritten like this: 


    func personFromData(data:NSData?) -> Result<Person, String {
        guard let theData = data else {
            return Result(error:"data was nil")
        }
        let json = JSON(data:theData)

        guard let firstName = json["firstName"].string else {

            return Result(error:"missing firstName in JSON")
        }
        guard let lastName = json["lastName"].string else {
            return Result(error:"missing lastName in JSON")
        }
        return Result(value:Person(firstName:firstName, lastName:lastName))
    }

This is much better. Observe that the code to deal with errors is next to the error condition that was tested. Also, once all of the error checking is out of the way, the code that continues on for the no-error situation is no longer indented several levels deep. 

Guards can be used to check any condition -- they aren't just tied to Optional unwrapping. For example: 

        guard index <= 3 else {
            return Result(error:"Index out of bounds")
        }

This is not drastically different from what could be done before, however, the nuance is interesting. The alternative looks like this: 

        if index > 3 {
            return Result(error:"Index out of bounds")
        }

The version using `guard` reads better because the expression indicates the success state. It reads more like an assertion. Also, with guard the compiler requires a return in the error clause. A common programming mistake is to log something about the error but forget to return, allowing program execution to continue as though there was not an error. 

Naming Things


In the first code examples above, we used the common convention 
    if let data = data {

With `guard let,` we can no longer do this because the new constant isn't constrained within a new scope. We have to use a different name for the Optional and non-Optional versions of a variable or constant. Naming things is hard, but the benefits of using `guard` are worth it. 

Swift continues to evolve and become even more powerful and enjoyable to use. 



No comments:

Post a Comment