Hello All!
I'm trying to pick back up some [work]( [Pitch] Result: Codable conformance and async "catching" init ) originally started by @ktoso : better async support for Result. Overall these two proposed changed are quite small. Originally, he had suggested added the async catching init. And this is something I think will be very appreciated.
The second addition, of a result-based Task accessor, was a little more of a whim. I think it's a nice little thing, but I also haven't found myself needing it very often. I took the time to write it up and make the case for it. However, I'm pretty open to push back on this one. So, if you think it isn't worth it and/or a bad idea, please let me know.
It's short, so I'm just including most of the proposal text here. But I've also opened a PR, and that's a great place for editorial feedback.
Motivation
The existing Result.init(catching:) initializer is a useful tool for transforming throwing code into a Result instance. However, there's no equivalent overload that can do this for asynchronous code. While this isn't particularly difficult to write, because of the utility, it ends up being manually duplicated in many code bases.
Such an initializer would make the following possible:
let result = await Result {
try await asyncWork()
}
It could be useful to take this even further. A Task wraps up possibly-throwing asynchronous work, the output of which is exposed with an accessor. A programmer might want to transform this output, a thrown error, or possibly both. These are exactly the kinds of operations that the Result API is intended to express.
Having an asynchronous initializer helps make these two types more compatible. But, a Result-based accessor provides a more streamlined interface. It also matches the continuation overloads that accept Result types nicely.
Here's how that might look in practice:
let result = await Task {
try await asyncWork()
}
.result
.flatMap { transformValue($0) }
Proposed solution
Both problems can be solved with additions to the standard library. First, by adding an async overload of the catching initializer. And second, a convenience property on Task that provides access to a Result-based output.
Detailed design
Here are the two proposed API changes.
Result initializer
extension Result where Success: ~Copyable {
/// Creates a new result by evaluating a throwing closure, capturing the
/// returned value as a success, or any thrown error as a failure.
///
/// - Parameter body: A potentially throwing asynchronous closure to evaluate.
@_alwaysEmitIntoClient
public nonisolated(nonsending) init(catching body: () async throws(Failure) -> Success) async {
do {
self = .success(try await body())
} catch {
self = .failure(error)
}
}
}
Task accessor
extension Task {
@_alwaysEmitIntoClient
public var result: Result<Success, Failure> {
async get {
Result { try await self.value }
}
}
}
What do you think?