Error Handling in Swift: Might and Magic

If distance brings perspective, proximity brings understanding. Concepts that were remote and downright strange when I played with them in Haskell or read about them in Scala are blindingly obvious solutions to any number of problems now that I am programming in Swift.

Take error handling. As a concrete example, dividing two numbers, which will fail if the divisor is zero. This is how I would handle this in Objective-C:

NSError *err = nil;
CGFloat result = [NMArithmetic divide:2.5 by:3.0 error:&err];

if (err) {
    NSLog(@"%@", err)
} else {
    [NMArithmetic doSomethingWithResult:result]
}

By now, this practically seems like the natural way of doing things; I forget the convolutions I’m going through and how little they correspond to what I really want the program to do:

Fetch me something. If it fails, let me know so I can handle it.

I’m passing parameters, dereferencing pointers, returning things every time, and ignoring the return value some of the time. This is very disorganized code, really, for a number of reasons:

Each of these is a potential source of bugs, and each can be addressed in Swift in a more systematic manner. The first one, obviously, is a non-issue since Swift abstracts away the pointers. The way to get around the other two problems is enumerations.

• • • • •

Error-prone computations have two possible outcomes:

These are mutually exclusive—in our example, dividing by zero leads to an error, and everything else to success. Swift conceptualizes mutual exclusion by supporting enumerations, usually called “enums”. Here is an enum that defines the result of an error-prone computation:

enum Result<T> {
    case Success(T)
    case Failure(String)
}

An instance of this type is either a Success with an associated value or a Failure with a message describing the cause. Each case defines a constructor: the first one takes an instance of T as its parameter (the result value), and the second one a String (the error message). The Objective-C code snippet becomes, in Swift:

var result = divide(2.5, by:3)

switch result {
case Success(let quotient):
    doSomethingWithResult(quotient)
case Failure(let errString):
    println(errString)
}

All in all, a bit wordier but much better! The switch statement lets me name the constructor arguments (quotient and errString) and access them in my code, and I can then process the result based on the outcome. All the issues are addressed:

Most importantly, the code expresses exactly what I intended: computing something and handling any errors. It maps directly to my concept of the task.

• • • • •

Now let’s consider a richer example. What if I want to process the result? Let’s say I want a function that will take a division result and compute a magic number from it by finding the least prime factor of the quotient and taking the logarithm. (There is nothing magic about the computation; I just picked operations at random.) The code would look like this:

func magicNumber(divisionResult:Result<Float>) -> Result<Float>
    switch divisionResult {
    case Success(let quotient):
        let leastPrimeFactor = leastPrimeFactor(quotient)
        let logarithm = log(leastPrimeFactor)
        return Result.Success(logarithm)

    case Failure(let errString):
        return Result.Failure(errString)
    }
}

Simple enough. But now that I have my magic number, I want to get the magic spell it corresponds to. So I write it up like this:

func magicSpell(magicNumResult:Result<Float>) -> Result<String>
    switch magicNumResult {
    case Success(let value):
        let spellID = spellIdentifier(value)
        let spell = incantation(spellID)
        return Result.Success(spell)

    case Failure(let errString):
        return Result.Failure(errString)
    }
}

Now, however, I have a switch statement in each function, and they look identical. Moreover, those methods really only want to run computations on the success value. The error handling a repetitive distraction.

Whenever things are repetitive, it pays to look for an abstraction. Again, Swift has the tools. Enums in Swift can have methods, and I can remove the need for these switch statements by creating the following map method on the Result enum:

enum Result<T> {
    case Success(T)
    case Failure(String)

    func map<P>(f: T -> P) -> Result<P> {
        switch self {
        case Success(let value):
            return .Success(f(value))
        case Failure(let errString):
            return .Failure(errString)
        }
    }
}

The map method, so named because it maps Result<T>s to Result<P>s, is straightforward:

Yet simple though it is, map allows downright sorcery. Because map handles the error states, I can rewrite magicNumber and magicSpell strictly in terms of the primitive operations:

func magicNumber(quotient:Float) -> Float {
    let lpf = leastPrimeFactor(quotient)
    return log(lpf)
}

func magicSpell(magicNumber:Float) {
    var spellID = spellIdentifier(magicNumber)
    return incantation(spellID)
}

Which then lets me get the magic spell like so:

let theMagicSpell = divide(2.5, by:3).map(magicNumber)
                                     .map(magicSpell)

Or, if I don’t need those methods in the first place:

let theMagicSpell = divide(2.5, by:3).map(findLeastPrimeFactor)
                                     .map(log)
                                     .map(spellIdentifier)
                                     .map(incantation)

This is tremendous. The need for error handling during computations is abstracted away. All I need to do is specify what computations I want to apply, and map will take care of propagating any errors.

This doesn’t mean that I will never have to write a switch statement. At some point I will need to either output the error or output the final result of the chained computations. But that is one switch statement, at the very end of processing—no need to write intermediate methods that process the Result types; those need only concern themselves the success values.

Sorcery, I say!

• • • • •

None of this is academic. Abstracting away error-handling is very useful when dealing with transformations of data: how often do we request data from a server that returns a JSON string or an error, and then want to transform the string into a dictionary, then map the dictionary into an object, which is in turn passed to the presentation layer which creates four display objects from it? The Result enum allows all the transforming methods to be written as if the data they received were always valid, and for errors to still propagate via calls to map.

If you haven’t seen this before, take a second to think about it. Play with the idea a little (once the code above compiles; the compiler still doesn’t handle the IR for generic enums). I think you’ll start appreciating the power.

• • • • •

If you’re into math, you may have noticed a bug in the example I gave. Logarithms are not defined for negative numbers and Floats can be negative. Therefore log would not in reality return a Float, but a Result<Float>. Passing it to map would then result in nested Results which would break our nice plan of processing only primitives. Don’t worry, there’s a solution to this. You can probably come up with it on your own. For those who don’t care to, I will discuss it in my next post.

 
741
Kudos
 
741
Kudos

Now read this

The Culmination: Part I

A big part of programming productively is figuring out the right level of abstraction for any given problem. The answer is not unique, though; there are many approaches to solving the same problem: procedural, object-oriented,... Continue →