![](/style/images/good.png)
![](/style/images/bad.png)
Using Swift Result and flatMap
source link: https://useyourloaf.com/blog/using-swift-result-and-flatmap/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK