Extending the Result Type - More Expressive Error Handling

Those aren't the same methods as this proposal, non? It's certainly been suggested many times to add synthesized computed properties or methods named after the enum cases, to do both pattern matching isCase: Bool and extract the associated value case: AssociatedValue?.

However, this calls for adding closure based methods with @discarableResult returns for chaining. It would generalize nicely to async types as well, such as Promise.onSuccess { value in ... }, Signal.onFailure { error in ... }, etc.

Not knowing that the @GLanza's proposal already existed and the fact that his proposal would bring a lot of features to Swift lead me to believe that it was unlikely to happen "any time soon". But I definitely didn't say that what the core team is asking (the Result being handled "by some more general language feature that applies to all enums in Swift automatically") is unlikely, due to the fact that it already is being handled by more general language features. Additionally, as @sveinhal pointed out the methods have changed from the previous proposal and new language features such as @discardableResult make the newly proposed methods more appealing.

I have updated the functionality, you can read more about it in the "Updated Functionality" section in the post describing the proposal. The updated functionality has room for improvement, though, so please take a look in the "Important Note" and the "Updated Functionality Implementation" section and please share your suggestions and feedback.

Show an example

The new functionality allows for Result chaining, as shown here:

enum GetError: Error {
    case UnexpectedError
}
enum GetSumError: Error {
    case UnexpectedError
}

func getNumber() -> Result<Int, GetError> {
    if Bool.random() {
        return .success(Int.random(in: 0...100))
    } else {
        return .failure(.UnexpectedError)
    }
}

func getSum(from number: Int) -> Result<Int, GetSumError> {
    if Bool.random() {
        return .success(Int.random(in: 0...100) + number)
    } else {
        return .failure(.UnexpectedError)
    }
}

getNumber()
.failure { error in
    print(error, "Failed to get first value")
}
.success{ value -> Result<Int, GetSumError> in
    print(value,  "is the first value")
    return getSum(from: value)
}
.success { value in
    print(value, "is the second valuet")
}
.failure { error in
    print(error, "Failed to get second value")
}

My point is that I believe they should possible for any enum. A more generalized approach is needed to address this issue which really is accessing associated values. If associated values were accessible with a better ergonomics, this problem was not existing.

1 Like

I am not arguing that there are no use-cases for your proposal or that it is not needed, instead I think that your proposal would be really useful to many users and that its addition to the language would be appreciated by Swift developers. What I am saying is that I believe that error-handling is a big part of Swift (and programming in general), even more so when making an API your self. Therefore I strongly believe that more expressive APIs for error-handling should be implemented in Swift, due to the fact that they would be beneficial to many users of the Swift language.

Isn't your new .success function just a .map, which already exists?
Also, @discardableResult isn't exactly new, it's been around for three years now.

However, I agree that the onSuccess and onFailure methods are useful. And I think they should be named like that, with the onā€¦ prefix. I do not think you should conflate those concepts with other similarly named methods with different signatures, and completely different semantics.

1 Like

Yeah right... I agree that @discardableResult isn't a good example of a new Swift feature.

There's been some controversy surrounding the naming of the success and failure methods, so please vote on what you think is the best name. Thank you!

  • success, failure
  • onSuccess, onFailure

0 voters

My apologies. The closure based methods are renamed versions of the original proposed fold(onSuccess:onFailure:). The core team had similar feedback about that:

The fold method should be removed. The core team agrees that there is utility to include something like this function, but it is best to take time to determine the best name for this, and it can be added as a separate additive proposal later. Furthermore, fold is essentially a switch expression; it may be better to simply add a general language feature for that than to add a special version of it for Result .


This is precisely the core team's feedback.

5 Likes

Again, this is different from the current proposals, and fold does indeed fit better as a general language construct for any kind of enum. I agree with that.

But this is isnā€™t a fold. Also, as I mentioned, these onā€¦ methods generalize beyond enums, and fit well for classes that represent some kind of future state, or signal handlers, etc.

1 Like

In the thread about extracting enum payloads I proposed to extend the syntax of anonymous functions with pattern matching abilities (copying the full text here to keep the syntax highlighting intact):

Iā€˜d like to propose a different solution: what about extending the syntax of anonymous functions to enable pattern matching on its arguments, i.e.

{ case .foo(let val) in val }

This would pattern match the argument and in the matching case return the result of the function wrapped in .some (i.e. .some(val)) and in the non matching case return nil.

This would keep all the safety and power of pattern matching, including elegant handling of multiple parameters where I might only be interested in some of them and nestings, e.g.

{ case .foo(_, .baz(let x, _), let y) in x * y }

-Thorsten

But this is not a "future state" if you have the result value, then the state is synchronous. Not future. on... gives me the idea that the value is not known yet, which is untrue. You do have an instance. you just want to inspect it with a better syntax. In short, you do want to access its associated value in one case or the other, just like you want to do with Optional, or any other case of an enum that for you represents a result.

The way this is usefully different from a fold ā€” that it makes it easy to ignore one case of the Result ā€” seems actively undesirable. I suppose it'd at least be explicit in the source that that's what you'd be doing, though.

8 Likes

Sure. I never claimed that is was. But it does represent a failable state, and you might want to access it in a way that is consistent across other failable states, such as futures etc. In that way, it has more semantic similarities with other such types, that is does other enums.

Sure, its possible to use just an onSuccess handler, just as you can easily write an if without an else clause. It is also different from a fold in that it returns itself, which is also useful.

For people used to the chaining writing style, reading an onSuccess without a corresponding onFailure is no more or less a code smell than reading an if without an else. And especially when chaining many maps, it's useful to be able to insert on onSuccess (or ifSuccess, onFailure, whatever) anywhere in the chain.

1 Like

The extended Result-based error-handling can be really useful for multiple cases, due to its flexibility. For instance, what you describe as being: ā€œactively undesirableā€ might be desirable in some cases where errors-handling can be omitted; and maybe should for the specific use-case. Besides, many people use:

if case let .success(value) = result {
	// Handling value 
}

// Omitting error

This is a current approach to error-handling that is already used by developers and that allows error-handling to be omitted. Hence, I cannot see how this proposal would be ā€œactively undesirableā€. Since omitting error-handling is already possible and with the implementation of the proposal, developers will be more likely to handle errors, due to the fact that the proposal makes error-handling easier. Other features of this proposal include enabling errors thrown to be fluently propagated from the function that they were thrown into a Result error-handler with an API that conveys that to the developer. Additionally, the proposal would enable developers to create a ā€˜Result-Chainā€™ for handling multiple Results in an expressive and concise manner (the ā€˜Result Chainingā€™ would be enabled by the code in the ā€œUpdated Functionality Implementationā€ section). These powerful features are not currently present in the Swift language and the proposal is an attempt to change that. (Also for those suggesting that do-try-catch can be used for Chaining; although true, this method lacks the ability to fluently and uniquely handle errors and is, in my opinion, less expressive).