Result Type Proposal:
This is a proposal that I think could make error-handling easier and more expressive than the currently used Result
API. The problem with the current Result
API is that, although throwing an error can be really intuitive, error-handling is much less intuitive. What I propose is an extension to the current API that embraces a more modern swift syntax.
Proposed Syntax:
enum GetError: Error {
case UnexpectedError
}
func getNumber() -> Result<Int, SendError> {
//...
guard let number = optionalNumber else {
return .failure(.UnexpectedError)
}
return .success(number)
}
getNumber()
.success { value in
print(value, "is the first number value.")
}
.failure { error in
print("An unexpected error occurred while trying to get the first number:", error)
}
Updated Functionality*:
getNumber()
.success { value in
print(value, "is the first number value.")
}
.failure { error -> Result<Int, GetError> in
print("An unexpected error occurred while trying to get the first number:", error)
return getNumber()
}
.success { value in
print(value, "is the second number value.")
}
.failure { error in
print("An unexpected error occurred while trying to get the second number:", error)
}
Important Note:
*My implementation (you can read it at the "Updated Functionality Implementation") of the "Updated Functionality" uses the struct
Data Type. So if you find a way to get the updated functionality working with the existing enum
based Result
Type implementation, please send it to me. If you've got any suggestions or feedback feel free to share it! Thank you!
Error Handling Improvements:
When throwing an error ( like in the "send" function), the new proposed API is identical to the current API. But what is different, is the way an error can be handled which is much more declarative and intuitive that the current API. That's because you don't need to use a switch
or an if
statement, as it is currently being done.
Implementation:
This new more declarative syntax could be achieved by extending the Result
Type (implemented with the help of @sveinhal) as shown here:
extension Result {
@discardableResult func success(_ handler: (Success) -> ()) -> Self {
if case let .success(value) = self {
handler(value)
}
return self
}
@discardableResult func failure(_ handler: (Failure) -> ()) -> Self {
if case let .failure(error) = self {
handler(error)
}
return self
}
}
Updated Functionality Implementation:
struct Result<Success, Failure> {
private let value: Success?
private let error: Failure?
private init(value: Success? = nil, error: Failure? = nil) {
self.value = value
self.error = error
}
}
extension Result {
static func success(_ value: Success) -> Self { // Create a `Success` value to return as a `Result`
Self(value: value)
}
static func failure(_ error: Failure) -> Self { // Create a `Failure` value to return as a `Result`
Self(error: error)
}
private static func empty() -> Self { // Create an empty `Result` for when the `success` or `failure` method has executed to prevent the other method (if there is any) from running
Self()
}
}
extension Result {
struct Handlers{
typealias result<S, F> = Result<S, F>
typealias success = (Success) -> ()// The simple `success` method:
// getNumber().success { print($0) }
typealias failure = (Failure) -> ()// The simple `failure` method:
// getNumber().failure { print($0) }
typealias successResulting<S, F> = (Success) -> result<S, F>// The `success` method that
// returns a `Result` on its own:
// getNumber().success { _ -> Result<Int, GetError> in return getNumber() }
typealias failureResulting<S, F> = (Failure) -> result<S, F>// The `failure` method that
// returns a `Result` on its own:
// getNumber().failure { _ -> Result<Int, GetError> in return getNumber() }
typealias autoSuccess = () -> () // The `success` method that takes no input and returns `Void`
typealias autoFailure = () -> () // The `failure` method that takes no input and returns `Void`
typealias autoSuccessResulting<S, F> = () -> result<S, F> // The `success` method that takes no input and returns a result
typealias autoFailureResulting<S, F> = () -> result<S, F> // The `failure` method that takes no input and returns a result
}
}
extension Result {
@discardableResult func success(_ handler: Handlers.success) -> Self {
if let value = value {
handler(value) // If a value exists run the `success` handler with it
}
return self // Return self to retain the chain
}
@discardableResult func failure(_ handler: Handlers.failure) -> Self {
if let error = error {
handler(error) // If an error exists run the `failure` handler with it
}
return self // Return self to retain the chain
}
}
extension Result {
func success<S, F>(_ handler: Handlers.successResulting<S, F>) -> Handlers.result<S, F> {
if let value = value {
return handler(value) // If a value exists run the `success` handler with it
}
return .empty() // Return an empty `Result<S, F>` to retain the chain
}
func failure<S, F>(_ handler: Handlers.failureResulting<S, F>) -> Handlers.result<S, F> {
if let error = error {
return handler(error) // If an error exists run the `failure` handler with it
}
return .empty() // Return an empty `Result<S, F>` to retain the chain
}
}
extension Result {
func success(_ handler: @autoclosure Handlers.autoSuccess) -> Self {
return success { _ in // Use the existing `success` method and just ignore the value
handler()
}
}
func failure(_ handler: @autoclosure Handlers.autoFailure) -> Self {
return failure { _ in // Use the existing `failure` method and just ignore the error
return handler()
}
}
}
extension Result {
func success<S, F>(_ handler: @autoclosure Handlers.autoSuccessResulting<S, F>) -> Handlers.result<S, F> {
return success { _ in // Use the existing `success` method and just ignore the value
return handler()
}
}
func failure<S, F>(_ handler: @autoclosure Handlers.autoFailureResulting<S, F>) -> Handlers.result<S, F> {
return failure { _ in // Use the existing `failure` method and just ignore the error
return handler()
}
}
}
Changes:
- The initial proposal was based on converting the
Result
Type fromEnum
toStruct
and adding the methods that enable the more expressive API, but after some discussion with you, the community, the new API was implemented as part of an extension of the current type. - I also changed the methods from
onSuccess
andonFailure
tosuccess
andfailure
so as to make the API more cohesive and to convey to the developer the fact that the API is synchronous (something that the previous methods might not have done). - I have updated the functionality (you can read it in the "Updated Functionality" Section) and therefore the implementation. The implementation has room for improvement, so please if you have any suggestions share them (for more go to the "Important Note" Section).