loading...
Cover image for Swift result types. Are they useless?

Swift result types. Are they useless?

hugh_jeremy profile image Hugh Jeremy ・3 min read

Fresh-faced, amateur, and impressionable: Swift is not my main jam. When setting out to write Amatino Swift, I was hungry for best-practice and good patterns. Amatino Swift involves lots of asynchronous HTTP requests to the Amatino API.

Asynchronous programming requires bifurcation at the point of response receipt. That is, an asynchronous operation may yield a success state or a failure state. Result types are a pattern widely espoused as a pattern for handling such bifurcation.

I'd like to open a debate over whether result types should be used in Swift. After some thought, it appears to me that they are useless. I propose that we would be better off encouraging newbies to utilise existing Swift features, rather than learning about and building result type patterns.

For the purposes of this discussion, let's assume that our code includes two functions, each of which handle a bifurcated state:

func handleSuccess(data: Data) {
    // Do stuff with data
}

func handleFailure(error: Error) {
    // Do stuff with error
} 

Inside these functions, we might implement code which is independent of our bifurcation pattern. For example, we could case-switch on Error type in order to present a meaningful message to a user.

Now to the bifurcation itself. A naive and simple pattern might be:

// This is bad code. Do not copy it!
func asynchronousCallback(error: Error?, data: Data?) -> Void {
    if (error != nil) {
        handleFailure(error!)
        return
    }
    handleSuccess(data!)
    return
}

There are myriad problems with this approach. We have no type safety over data. We do not control for cases where programmer error yields a state where both data and error are nil. It's ugly. More.

Result types propose to improve upon this pattern by defining a type such as:

enum Result<Value> {
    case success(Value)
    case failure(Error)
}

Which may be used like so:

func asynchronousCallback(result: Result<Data>) {
    switch result {
    case .success(let data):
        handleSuccess(data)
    case .failure(let error):
        handleError(error)
    }
    return
}

This pattern introduces type safety to both error and data. I suggest that it does so at too great a cost when compared to using inbuilt Swift features. Every asynchronous bifurcation now requires a switch-case statement, and the use of a result type.

Compare the result type pattern with one that uses the Swift guard statement:

func asynchronousCallback(error: Error?, data: Data?) {
    guard let data = data else { handleError(error ?? TrainWreck()) }; return
    handleSuccess(data)
    return
}

In this case, we have type safety over error and data. We have handled a case in which a programmer failed to provide an Error using the nil-coalescing operator ??. We have done it all in two lines of less than 80 char. A suitable error type might be defined elsewhere as:

struct TrainWreck: Error { let description = "No error provided" }

Bifurcation via a guard statement appears to me to have several advantages over result types:

  • Brevity. Functions handling asynchronous callbacks need only implement a single line pattern before proceeding with a type safe result.
  • Lower cognitive load. A developer utilising a library written with the guard pattern does not need to learn how the library's result type behaves.
  • Clarity. A guard statement appears to me to be more readable than a case-switch. This is subjective, of course.

What do you think? I am not a Swift expert. Am I missing something obvious? Why would you choose to use a result type over a guard statement?

Cover image - A bee harvests pollen from lavender on Thornleigh Farm

Posted on by:

hugh_jeremy profile

Hugh Jeremy

@hugh_jeremy

Making farm robots (https://thornleighfarm.com) & accounting APIs (https://amatino.io).

Discussion

markdown guide
 

Not a swift programmer, but in general the advantage of the ADT Result is composability and the use of higher order functions (map, mapError, flatMap, fold), while the example is focussing only on unwrapping the datatype.

 

I would argue that your last example, although an improvement over the initial one, still can be improved. Consider this API:

protocol API {
    func asynchronousCall(_ context: Context,
                   onSuccess: @escaping (ResponseType) -> (),
                   onFailure: @escaping (Error) -> ()) -> CancellationToken
}

// Invocation site
_ = api.asynchronousCall(context, onSuccess: { responseValue in
    // handle success
}, onFailure: { error in
    // handle error
})

In this example, the API solves the bifurcation problem for the caller completely and provides it with a clean type-safe way of handling success or failure.

The same can be achieved by unpacking the Result type with observer pattern, which removes the need for switch statement:

func asynchronousCallback(result: Result<ResponseType>) {
    result.handle(success: { responseValue in
        // handle success
    }, error: { error in
        // handle error
    })
}

Or even in Rx:

import RxSwift

protocol API {
    func asynchronousCall(_ context: Context) -> Single<ResponseType>
}

// Invocation site
_ = api
    .asynchronousCall(context)
    .subscribe(onSuccess: { responseValue in
        // handle success
    }, onFailure: { error in
        // handle error
    })

To summarize, the API should provide the caller with an unambiguous representation of the result of the operation. Result type provides such disambiguation. Variants of the callback function with optional parameters do not.

 

I don't develop in swift but find it interesting. That said, the result and option types in other languajes get syntactic sugar via the "do notation" or the "? operator", makes many things of what you are exposing way easier.

 

i also find result type strange...
BUT, maybe with higher order functions it could have a better meaning: infoq.com/news/2019/01/swift-5-res...