Error Handling in Swift: Might and Magic—Part II

In my previous post, I showed that defining a map method on the Result enum allows chaining of a series of transformations to a result without caring whether the result was a success or a failure until the value was needed for output. I pointed out at the end, though, that there was a problem with certain types of methods. I’ll address that issue here.

• • • • •

A brief recap. The map method allows chaining any single-input-single-output method on a result, like so:

var finalResult = someResult.map(f1)
                            .map(f2)
                            .map(f3)

The outcomes are what we would expect:

In other words, the failure is propagated. This is awesome, and works great with error-proof computations, such as additions, multiplications, array length, etc. But what happens if one of the computations can fail? In the example I nonchalantly had:

let result = divide(a, by:b)
let logResult = result.map(lowestPrimeFactor).map(log)

However, a realistic log function would be declared as func log(num:Float) -> Result<Float>. It would return a Result because log is undefined for negative numbers. So if I were to apply map as above, I’d end up with this monster as my return type: Result<Result<Float>>. I could then only call map with functions that took a Result<Float> as a parameter, completely breaking my abstraction.

Clearly, map is not up to the task. What I need is a second method, similar to map, but that will know how to funnel, so to speak, a Result through a function that takes a value but returns a Result. I need this:

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

Again, a very simple implementation:

It’s actually even simpler than map, despite serving a more complex case. Looking back at the first example, if f2 can return an error, the could turns into:

var finalResult = someResult.map(f1)
                            .flatMap(f2)
                            .map(f3)

The outcomes are a bit more complex but make every bit as much sense:

In short, things will be exactly as we would expect them to be: the error that breaks the computation is propagated, and if no error occurs, the computation returns a valid result. For reference, this is what the magic spell example from my previous post should actually look like:

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

So I can mix and match map and flatMap calls as needed by the functions I want to apply—and I still keep the benefit of delayed error handling.

• • • • •

These two posts have dealt with synchronous error handling. The truth is, though, that a good chunk of error-prone code is nowadays asynchronous; we try to maintain UI responsiveness by throwing as many things as possible on background threads, and it’s usually network or file IO issues that break things, which typically take callback blocks. In a later post I’ll take a look at approaches for dealing with that.

Update: I changed the name of the function introduced in this article to flatMap from funnel, after discussing with @cocoaphony and @lightfiend the pros and cons of the name and deciding that my dislike of flatMap is less well-founded than I initially felt.

 
190
Kudos
 
190
Kudos

Now read this

You Are More Than a Coder

The answer — by demonstration — would take care of that, too. — Isaac Asimov, The Last Question From time to time, I stumble across something beautiful and true. It happened recently, and this is me trying to share it. It has a formal... Continue →