I like the idea of guard .. catch
, however I think it's not enough and the ideal solution would be for compiler to recognise try Result.get()
as typed throw and automatically bind the error in catch
statement to Failure
type held by Result
:
let result: Result<T, F> = ..
// Result.Failure is bound to `let error` either implicitly or
// explicitly using variable declaration following
// the catch statement.
guard let value = try result.get() catch [let error: F] {
// ...
}
In cases when the statement is complex, compiler could fallback to generic Swift.Error
, so in pseudo language:
let resultA: Result<T, F1> = ..
let resultB: Result<U, F2> = ..
<InferredFailure>
guard let valueA = try resultA.get(),
let valueB = try resultB.get()
catch [let error: InferredFailure]
where
InferredFailure = F1 == F2 ? F1 : Swift.Error
{
// ...
}
For any other throwing functions, compiler could continue using Swift.Error
, i.e:
func fn() throws -> Value {
throw MyError()
}
guard let value = try fn() catch [let error: Swift.Error] {
// ...
}
Same concept applies when unpacking result and calling any other throwing function within the same guard .. catch
statement, i.e:
func fn() throws -> Value {
throw MyError()
}
let result: Result<T, F> = ..
guard let value = try fn(),
let otherValue = try result.get()
catch [let error: Swift.Error] {
// ...
}
The one could also explore the idea of making try
statements act as early return in contexts returning Result<T, F>
:
func a() -> Result<T, F1> {
let value = try b().mapError({ .. }).get()
// Do something with value ...
}
func b() -> Result<T, F2> {
// ..
}
which compiler could translate into:
func a() -> Result<T, F1> {
let value: Value
do {
value = try b().mapError({ .. }).get()
} catch /* let error: F1 */ {
// Early return
return .failure(error)
}
// Do something with value ...
}
func b() -> Result<T, F2> {
// ..
}
Edge case here a function that both throws
and returns a Result<T, F>
which makes little sense to me, hence it could be tackled in a few ways:
-
Can be made illegal.
-
Ignore return type and act as a regular throwing function. That should provide backward compatibility with existing code. Compiler perhaps could issue a warning to notify the developer that perhaps they should stick either with exceptions or Result<T, F>
.
-
Different keyword can be used such as unwrap
instead of try
to avoid collisions with exceptions, which could be made available only if the function returns Result<T, F>
and act in similar fashion as early return ? operator in Rust. But that would be the first class support for Result
which the core team seem to have some issues with for reasons that are unknown to me.
func fn() -> Result<Int, FailureA> {
let resultA: Result<Int, FailureA> = ..
let resultB: Result<Int, FailureB> = ..
// returns .failure(error) if resultA contains failure,
// otherwise assigns valueA and continues execution.
let valueA = unwrap resultA
// returns .failure(error) if resultB contains failure,
// otherwise assigns valueB and continues execution.
let valueB = unwrap resultB.mapError { /* map to FailureA */ }
// Executes when both valueA and valueB are available.
return .success(valueA + valueB)
}
Compiler could generate all the boilerplate that we complain about here, such as match success, failure branches and do the implicit return. This approach also does not require re-engineering the guard statement as they become unnecessary but it would require the introduction of a new keyword specifically for Result<T, F>
type. In my opinion this is a lesser evil than bending the existing exception mechanism to work for Result<T, F>
.
Either of the suggestions above would enable writing concise and strictly typed, yet powerful code with nice early returns, while also providing ability to map returned errors before returning them. However someone with more experience should refine my proposal if either of this is ever going to be (even) considered to be implemented. I am obviously just throwing some ideas.