Using Swift Result and flatMap

The Swift Result type is handy way to capture the results of a throwing expression that you need to execute on a background thread. Use flatMap to chain several results together.

The Swift Result Type

Apple added the Result type to the Swift Standard Library in Swift 5. It’s an enum with a success and failure case both of which have an associated value. You can use any type for Success but Failure is constrained to Error:

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

Using Result To Clean Up Completion Handlers

The Result type is often used to clean up the completion handlers of asynch API’s like URLSession. The dataTask(with:completionHandler:) method has a completion handler with three optional parameters:

let task = session.dataTask(with: url) { data, response, error in
  // what combinations of data, response
  // or error are expected here?
}

If you’re unfamiliar with this API it can be hard to know that the completion handler is called either with a non-zero error or with data and a response. You can better represent the success and failure cases with Result:

extension URLSession {
  func load(_ url: URL,
  completionHandler: @escaping (Result<(URLResponse, Data), Error>)
  -> Void) -> URLSessionDataTask {
    let task = ...
    return task
  }
}

Or if we handle the URLResponse for the caller and pass the completion handler data or an error (see Gist for details):

extension URLSession {
  func load(_ url: URL,
  completionHandler: @escaping (Result<Data, Error>) -> Void)
  -> URLSessionDataTask {
    let task = ...
    return task
  }
}

The caller switches over the result to get the data or error:

let task = URLSession.shared.load(url) { result in
  switch result {
  case .success(let data): print(data)
  case .failure(let error): print(error)
  }
}

That’s great but there’s another, easy to miss, use for Result.

Create A Result From A Throwing Expression

You can create a Result from a throwing closure:

// Result<[Country], Error>
let result = Result {
  try JSONDecoder().decode([Country].self, from: data)
}

The Result captures the return value of the expression in the .success value or any thrown error in the .failure value. You can convert a Result back to a throwing expression using get():

let countries = try result.get() // can throw

A Practical Example

That’s all a bit theoretical, here’s a practical example. I want a method on my CountryStore class that loads and decodes country data from a file. You might start by creating a throwing method that runs on the main thread:

final class CountryStore: ObservableObject {
  @Published var countries = [Country]()
  @Published var error: Error?

  func load(_ url: URL) throws {
    let data = try Data(contentsOf: url)
    countries = try PropertyListDecoder().decode([Country].self,
                    from: data)
  }
}

A better way is to make this a non-throwing method that loads and decodes on a background thread:

func load(_ url URL) {
  DispatchQueue.global(qos: .background).async { [weak self] in
    // Non-throwing block to execute
  }
}

Since we can’t throw errors in the block we can wrap the throwing methods with a Result. First to load the data:

let result = Result { try Data(contentsOf: url) }

This gives us a result of type Result<Data, Error>.

Using flatMap

We can chain operations together transforming the result each time with a flatMap (I never remember that it’s flatMap and not flatmap):

.flatMap { data in
  Result {
    try PropertyListDecoder().decode([Country].self, from: data)
  }
}

The flatMap unwraps the second result for us giving us a final result of type Result<[Country], Error>. A plain map would have given us a nested result of type Result<Result<[Country],Error>, Error>.

We then dispatch back to the main thread to update our published properties with the result:

DispatchQueue.main.async {
  switch result {
  case .success(let countries):
    self?.countries = countries
  case .failure(let error):
    self?.error = error
  }
}

We can clean up the unwieldy syntax with an extension on Data:

extension Data {
  static func contentsOf(_ url: URL,
    options: Data.ReadingOptions = []) -> Result<Data,Error> {
    Result { try Data(contentsOf: url, options: options) }
  }
}

Doing the same for PropertyListDecoder:

extension PropertyListDecoder {
  static func decode<T: Decodable>(_ data: Data) -> Result<T,Error> {
    Result { try PropertyListDecoder().decode(T.self, from: data) }
  }
}

Our two throwing methods to load and decode data then reduce to this:

let result: Result<[Country],Error> = Data.contentsOf(url)
            .flatMap(PropertyListDecoder.decode)

The only downside is that we need to help the compiler with the result type. The full method:

final class CountryStore: ObservableObject {
  @Published var countries = [Country]()
  @Published var error: Error?

  func loadStore(_ url: URL) {
    typealias CountryResult = Result<[Country], Error>

    DispatchQueue.global(qos: .background).async { [weak self] in
      let result: CountryResult = Data.contentsOf(url)
                  .flatMap(PropertyListDecoder.decode)
      DispatchQueue.main.async {
        switch result {
        case .success(let countries):
          self?.countries = countries
        case .failure(let error):
          self?.error = error
        }
      }
    }
  }
}

Not exactly a work of art compared to the throwing version but it has more to do. A method to save the store is more compact. The result is of type Result<Void,Error> so we only need to look at the failure case:

func saveStore(_ url: URL) {
  DispatchQueue.global(qos: .background).async { [weak self] in
    let result = PropertyListEncoder.encode(self?.countries)
                 .flatMap { Data.write($0, to: url) }
    if case .failure(let error) = result {
      DispatchQueue.main.async {
        self?.error = error
      }
    }
  }
}

See Swift If Case Let for a reminder on how to use if-case-let to avoid a full switch statement when you only care about one case.

Read More