Result types in Swift. Are they useless?

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

Originally posted at The Practical Dev