Swift result types. Are they useless?

Hugh Jeremy - Oct 28 '18 - - Dev Community

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
} 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . .
Terabox Video Player