The primary reasons for us to use async would be to avoid a lot of nesting. The obvious, type-safe, way to handle the bove result would be a simple switch, like this:
switch result {
case .success(let products):
// Handle success
case .failure(let error):
// Handle error
}
but now we are back at nesting. If we want to call another api inside the success we are nested one level more. For each call, the nesting increases.
The only solution we have been able to come up with is
guard case .success(let products) = result else {
handleFailure(result)
return
}
but then the handleFailure() func needs to do pattern-matching again to unwrap the error, even though we already know that it must be the .failure case.
Is there a more elegant and ergonomic solution?
For a various of reasons we do not want to have the api throwing so please do not suggest that.
let products: [Products]
let result = await fetchProducts()
switch result {
case .success(let p):
products = p
case .failure(let error):
handleError(error)
return
}
// use products
I don't think it's fair to needlessly wrap the result and then expect some magic way to unwrap it without ceremony. By refusing to consider alternatives, you've created the problem for yourself. I don't think it's proper to expect the language to help you out of the hole you dug for yourself.
You could create an extension on the Result type to preserve the type of the failure
extension Result {
func getFailure() -> Failure? {
if case .failure(let failure) = self {
return failure
}
return nil
}
}
let result = await fetchProducts()
if let error = result.getFailure() {
handleError(error)
return
}
let products = try! result.get()
// use products...
This is a solution that is close to the ergonomics of the one i proposed myself but might be a bit easier to read. I would like to avoid the try! but I think it is a pragmatic solution in this case.
Result where Failure == Error is exactly the same thing as throws. Nobody else needs to suggest it, because you already did, and adopted it, just with a harder to use spelling.
As @JuneBash said, this is what map and flatMap are for. If you throw zip into the mix as well and you can do quite a bit. Good stuff about how to use all three together to make declarative data manipulations over at Pointfree in their collection on those three operations: Map, Zip, Flat‑Map
The syntax is not great now, but if you want to make async calls one by one, try this:
func doSomething() async {
let productsResult = await fetchProducts()
let products: [Product]
switch productsResult {
case .success(let success): products = success
case .failure(let error): return await handleError(error)
}
let productImagesResult = await fetchProductImages(products)
// ...
}
There ere some discussions about improving of syntax:
guard-catch with typed throws can make such things easier.
Result can be easily transformed to typed throw, if these features come into play. So may be in future soothing like this can be possible:
extension Result {
func getValueOrThrow() throws Failure -> Success {
switch self {
case .success(let success): return success
case .failure(let error): throw error
}
}
}
func doSomething() async {
guard let products: [Product] = await fetchProducts().getValueOrThrow() catch {
return await handleError(error) // the error here is of concrete type, the same as in Result
}
let productImagesResult = await fetchProductImages(products)
// ...
}