Harlan Haskins

Software Engineer | About | Projects | Resume | Contact

Swift and Haskell: `do` or Die

Note: This post requires cursory knowledge of Monads and, ideally, a teensy bit of Haskell. You can probably get away with reading Javier Soto's piece Functor and Monad in Swift.


Swift 2 introduced a robust error-handling system that abstracts away old Cocoa NSError ** handling.

In Swift 1.2, using something like NSJSONSerialization required a very awkward, pass-by-reference error model, that feels very foreign to Swift.

func parseJSONData(data: NSData) -> AnyObject {
    var error: NSError?
    let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &error)
    if let error = error {
        println(error) // Error 'handling'
        return nil
    }
    return json
}

func jsonAsDictionary(json: AnyObject, inout error: NSErrorPointer) -> [String: AnyObject]? {
    if let cast = json as? [String: AnyObject] {
        return cast
    }
    error = NSError(domain: "JSONThing", code: 0x22334422, userInfo: nil)
    return nil
}

There's a lot of visual noise, and it's hard to pick out the real implementation details buried within the error handling logic.

Swift 2 abstracts NSError into a new system that feels like other languages' try/catch semantics.

That code, in Swift 2, becomes:

enum JSONError {
    case Cast
}
func parseJSONData(data: NSData) throws -> AnyObject {
    return try NSJSONSerialization.JSONObjectWithData(data, options: [])
}
func jsonAsDictionary(json: AnyObject) throws -> [String: AnyObject] {
    if let dict = json as? [String: AnyObject] {
        return dict
    }
    throw JSONError.Cast
}

The try is the key there. By using try you're promising either to handle the error, or pass the error onto the caller (which is marked with throws in the type signature)

Then we can use multiple of these functions within one do block, and fail into the catch when the first one fails.

do {
    let data = NSData()
    let json = try parseJSONData(data)
    let dict = try jsonAsDictionary(json)
} catch {
    println(error)
}

That's the crux of Swift error handling -- at some point in the function's lifetime, some caller has to catch the error. If not, it's a compile-time error.

And catch isn't some kind of expensive, stack-unwinding Exception. In fact, you can think of throws in a type signature as syntactic sugar for some kind of Either type.

Let's try de-sugaring the throws from the second example, into something that's Swift 2 compatible.

We start by defining an Either type. Either traditionally holds two values -- Left, which contains some kind of error, and Right, which contains a valid value.

enum Either<T> {
    case Left(error: NSError)
    case Right(value: T)
}

For an added bonus, we'll implement Monadic bind for our Either type, to make it easier to chain multiple failable expressions. In this case, if the bound Either is .Left, we want to ignore the transformation and propagate out the existing error.

However, if the bound Either is .Right, we want to apply the transformation and return the result.

Since the result is an Either as well, you can chain multiple of these binds as much as you want.

extension Either {
    func bind(transform: T -> Either<T>) -> Either<T> {
        switch (self) {
        case .Left:
            return self
        case .Right(let value):
            return transform(value)
        }
    }
}

Now let's define those two functions from earlier, except using our Either type.

func parseJSONData(data: NSData) -> Either<AnyObject> {
    var error: NSError?
    let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.allZeros, error: &error)
    if let error = error {
        return .Left(error: error)
    }
    return .Right(value: json!)
}

func jsonAsDictionary(json: AnyObject) -> Either<[String: AnyObject]> {
    if let dict = json as? [String: AnyObject] {
        return .Right(value: dict)
    }
    return .Left(error: NSError(domain: "JSONThing", code: 0x22334422, userInfo: nil))
}

let jsonData = NSData(contentsOfFile: "someFile.json")
parseJSONData(jsonData).bind(jsonAsDictionary)

Well, there's even more code, since we still have to live in the old world and now transform it into our new world. But we've at least created a consistent abstraction that allows for native, descriptive errors.

The throws example from earlier is very similar behavior to do notation in Haskell. do notation wraps one or many monadic operations in a way that feels almost imperative.

Say we were interacting with some Swift-Haskell interface. We could work with that parseJSONData using do notation.



jsonDictionary = do
    let data = File "someFile.json" :: NSData
    parsedJSON <- parseJSONData data
    jsonAsDictionary parsedJSON

Or, by desugaring the do notation,

jsonDictionary = parseJSONData json >>= jsonAsDictionary
    where json :: NSData
          json = File "someFile.json"

They follow very similar semantics, by abstracting away and propagating failures throughout the sugared syntax.

This is, of course, an oversimplifcation of both technologies, but it's exciting to see functional concepts at the very core of Swift, and shaping its future.