Making a case for Result

Swift introduced the Result type in its 5.0 release (Apple documentation). Using it, we can represent both success and error cases with a single type. It is highly useful with asynchronous APIs that could either error out OR return some value when everything goes right. I hear some of you go, “what about synchronous APIs?”. Well, such methods can always “throw” errors when something goes wrong, but there is nothing stopping them from also returning Results

Without Result, the completion handler of an asynchronous API could be called using a tuple of type (Error?, SomeValueType?). Handling this tuple properly, entails some cumbersome and unnecessary unwrapping of optionals.

Before the advent of Swift’s Result, it was of course, not too difficult to write your own Result type, but having it as part of the language is always nice.

In this article, let’s take a closer look at Result and the operations it supports.

Definition

It is defined as a generic enumeration with 2 cases:

enum Result<Success, Failure> where Failure: Error {
 case success(Success)
 case failure(Failure)
}

The two generic type parameters are named Success and Failure. Success is not constrained, so it is free to be any Swift type, whereas Failure has to conform to the Error protocol.

Both the success and failure cases have associated types – of types Success and Failure respectively. Being a generic type, the exact type of a Result will be defined when you create one. The simplest Result I can think of is Result<Bool, Error>

let aResult = Result<Bool, Error>.success(true)

// or

let anotherResult = Result.success(10) // inferred to type Result<Int, Error>

Similarly, if you have an error type

enum ProgrammingError: Error {
  case outOfBounds
  case notFound
}

you can define results of the form

let terribleResult = Result<Int, ProgrammingError>.failure(.outOfBounds)

Handling a result

Being an enumeration, the most natural and the idiomatic way to handle a result is with the switch statement.

switch returnedResult {
  case .success(let value): print("This is exactly what I wanted! \(value)")
  case .failure(let err): print("Story of my life: \(err.localizedDescription")
}

Transforming Results

Result comes packaged with a few built-in transformation operations for often used tasks.

map:

func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure>

The map function accepts a closure (or another function) that operates on a successful value and returns another value of potentially another type (NewSuccess). The error type remains the same and any error, in fact, passes straight through.

let intResult = Result<Int, Error>.success(0)

let transformation: (Int) -> String = { value: Int in 
    return "\(value) as a string"
}

let transformedResult = intResult.map(transformation) // this is of type Result<String, Error>

flatMap:

This is map’s more interesting cousin

func flatMap<NewSuccess>(_ transform: (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>

The closure still accepts a value that represents a success, but the closure itself returns another result, whose success type is, potentially, different (NewSuccess). Because it returns a Result, it is possible to return a failure, but still of the same failure type. If the original result were an error, it passes straight through. 

The flatMap operator also does the nice thing of unwrapping the result, so that we end up with something like a Result<String, Error> instead of a Result<Result<String, Error>, Error>

let someResult = Result<Int, ProgrammingError>.failure(.outOfBounds)

let transformation = { (value: Int) in
  return Result<String, ProgrammingError>.failure(.notFound)
}

let anotherResult = Result<Int, ProgrammingError>.success(2)
let anotherTransformation = { (value: Int) in
  return Result<String, ProgrammingError>.failure(.notFound)
}

let transformedResult = someResult.flatMap(transformation) // still a .failure(.outOfBounds)

let anotherTransformedResult = anotherResult.flatMap(anotherTransformation) // the success is transformed to .failure(.notFound)

mapError and flatMapError:

As the names suggest, these two variations allow you to transform only the failure cases to something different. Original success cases remain the same.

Try hard until you succeed

You might find yourself wanting to transform a throwing function to a Result. A throwing function that returns a value maps nearly one-to-one to a Result. For this common case, the developers of the language have helpfully included an initializer that takes a throwing function.

init(catching body: () throws -> Success)

Any error thrown maps to a .failure case with that error as the associated value. A successful return value is mapped to a – you guessed it – .success case with the right associated value.

func doSomething() throws -> String {
  let randomNumber = Int.random(in: 0...10)
  if randomNumber.isMultiple(of: 2) {
    return "cool"
  } else {
    throw ProgrammingError.outOfBounds
  }
}

let superResult = Result(catching: doSomething)

// or

let superResultTwo = Result { 
  let randomNumber = Int.random(in: 0...10)
  if randomNumber.isMultiple(of: 2) {
    return "cool"
  } else {
    throw ProgrammingError.outOfBounds
  }
}

In both of the above examples, the “super” results are either a .success("cool") or a .failure(.outOfBounds)

The last method on the result that we will talk about is the not so well named (IMO) .get() – this does somewhat the reverse of the ‘catching’ initializer. It returns a value if the underlying result is a success, but throws on error if not. 

This allows the result to be used in the context of a do-catch, or for optional binding if you are alright with throwing any errors out the window.

Extending Result

To wrap this up, we will write a small extension that discards the failure case and replaces it with the provided default as a success value.

extension Result {
  func mappingErrorToDefault(value defaultValue: Success) -> Success {
    return (try? self.get()) ?? defaultValue
}

Success, the generic type parameter, is also available in extensions. This allows us to use it for accepting and returning values of the original type. 

(try? self.get()) evaluates to nil if the underlying self is an error case, in which case, we default to the provided success value.

This wraps up our little exploration of the Result type, and I am sure you will agree that it is fairly easy to use and should help make your code cleaner. It has a few more properties, out of the box, related to the Combine framework, but that will be the subject of a future blog post (knowing me, it will never come)

Thank you for reading!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply