Thanks for the additional examples and detail in your two recent posts. Between these and the post from @xwu above, I have a much better understanding of the desire for less abstract “Result” semantics rather than an unbiased “Either”, and I see the value.
So this changes my initial -1 on Result vs. Either to a +1, despite it still not feeling quite like a real citizen of Swift and the Apple frameworks ecosystem. It does at least represent a commonly used semantic + structural abstraction that’s better off as a common definition.
While I agree that row types would be ideal, language features like that seem so far off in the distance that we may never see them. Swift is product-typed biased right now, plain and simple. Tuples have no sum-based equivalent. Key paths only work with structs out-of-the-box. Enums are far less ergonomic than structs and a general Either type, while not ideal, is certainly better than nothing at all or waiting forever. I also fear that if/when we get row polymorphism, it'd be another feature that only works with product types.
In the spirit of keeping things focused to the original topic in this already very long review thread, might I propose that we move discussion of structural types and ‘Either’ out to another thread?
Since this thread is a review of a specific proposal for a ‘Result’ type, the question of how to design an ‘Either’ type and whether it is best a structural type is pretty far afield, I should think.
I agree with Xiaodi. If your opinion is that we should have a generic Either type over a "biased" Result type, that's totally reasonable feedback to leave here, but this is not the place to debate an actual design for an Either type or a general structural-sums feature.
Also, the review period for this proposal is technically over.
I suggest we all take a step back and return to the original proposal - Result. Either is a nice type and I'd like to have it at my disposal, but is it in conflict with the Result? No, it is another type. It is a more generalized version of Result. It does not conflict with Result. Focus on the semantics. Semantics of the Result type is well-known. Either I get a value T or an error E: Error. This is different from Either where I expect either a value T or another value U. Since semantics is so clear in these cases, the only way it makes sense for Result to be implemented is to have error constrained Error: Swift.Error - otherwise we are talking about Either type and that's not a topic of this proposal.
Is Result a backdoor to introducing typed throws? It is such a harsh way to put it, but even if it is, is that really a bad thing? Is allowing typed throws necessarily a bad thing? There are so many use cases where typed errors are useful so why not allow them. They don't have to be forced upon users, but they could be allowed. The way I see this, is that there is a pressing need for Result type. It's solves so many problems that the Swift community is experiencing today. It is potentially a building block of a better error handling system, that may or may not come. It allows us to make our code more expressive.
Could it become obsolete when we get better concurrency support? It is likely that its usage will drop significantly. But will it conflict will anything, will it limit us to do better? No.
What I loved about early days of Swift is that there was no huge fear of change. I understand that there is so much more responsibility these days, but openness to new things is what got Swift to this point of being a great language to work with. I would hate to see it stagnate.
It looks like you already switched your position here but I wanted to comment on the Either<Former, Latter> . I was also on the same camp when it came to naming but your comments that Either<Former, Latter> and Either<Latter, Former> should be equivalent solidify my stance on the general Either type.
There are cases when I do want to throw the error away. In my mind a Result's type main benefit is that is able to carry an error but I should be able to ignore that error if I do not care about it.
Here it is IMHO obvious that the .success case represents the fetched error, and the .failure case represents an error accessing the remote log.
I don't like Either because it is too generic. Case names like left/right, former/latter, and first/second are almost never relevant in an application of Either.
The migrator's primary and most important goal is to preserve the semantics of the program when moving to latest version. I'm not in favor of trying to adjust uses of Result from multiple third-party libraries, particularly when considering that it could be prohibitively difficult to be 100% accurate due to the stdlib one not being a drop-in replacement, and the fact that we may not have access to downstream clients of this APIs using the third-party Result.
We could potentially offer a separate "modernization" pass, similar to the ObjC modernizer, which can be opt-in and independent of moving to latest version (you could run it anytime after a project already moved to swift 5+), but I would recommend not to base decisions on the existence of such functionality.
Although the review period is over, please let me leave my comment. I have been thinking about it for days.
If I correctly understood what you intended in the comment below you linked,
it seems to suggest that JSONDecoder.decode(_:from:) returns Results instead of throwing errors.
I think usefulness of automatic propagation (throws) is not limited to "systemic errors". For example, if we have an API to decode JSONs manually, and if manual propagation (Result) is preferred for non "systemic errors", the API would return Results like JSONDecoder.decode(_:from:). Then decoding JSONs manually would be written like below with complex conversions.
func decodeFoo(from json: JSON) -> Result<Foo, DecodingError> {
return Result {
let a: Int = try json["a"].unwrapped()
let b: Bool = try json["b"].unwrapped()
let c: String = try json["c"].unwrapped()
return Foo(a: a, b: b, c: c)
}.mapError { $0 as! DecodingError }
}
If the API throws errors directly, it can be written much more simply as follows (assuming that throwing subscripts referred in SE-0148 are available).
func decodeFoo(from json: JSON) throws -> Foo {
let a: Int = try json["a"]
let b: Bool = try json["b"]
let c: String = try json["c"]
return Foo(a: a, b: b, c: c)
}
As seen in the example above, I think automatic propagation is also useful for non "systemic errors". So I prefer using Result more limitedly as @John_McCall commented.
Then I think it is better not to add Result to the standard library now. If Result is added to the standard library now, I am sure that it will be widely abused instead of throws. I guess 90% of actual use cases of Resulttoday are for asynchronous operations and typed errors. Because async/await and typed throws are not supported currently, Result will be the only way for the cases. If async/await and typed throws are introduced first (though I am not sure if typed throws should be supported), before Result, async/await and typed throws would be used in the cases. And the community could get experienced with them. Then Result could be used properly only when manual propagation is required.
I know this is late, but I'm changing my response to -1. I tried for fun to integrate antitypical/Result into PromiseKit and because the Result type requires a concrete Error type to be declared it made it impossible to use this form of Result.
I believe this was acknowledged in the thread with a potential solution, but it is not acknowledged in the proposal.
I don't see the point in adding a Result type to the stdlib if projects out there that need it cannot use it as proposed.
Further using antitypical’s Result was continuously tedious due to the need to always declare which Error was involved. Sometimes it seemed impossible to use it since a particular area I needed may return multiple types of error.
I think adding as proposed wouldn't help nearly as many people or projects as people have implied here.
Those sorts of difficulties are precisely why the proposal is unconstrained on the error type, making definitions that just expect Error possible, where they weren't with the constrained version.
Is the problem being addressed significant enough to warrant a change to Swift?
I think it should go further. Just adding Result is not enough, it only extends the "old" error handling besides try/catch. Both error handling methods should work together:
let foo: () throws -> String = ...
let result: Result<String, Error> = try?? foo()
// try??, or catch, or new keyword - no do { } catch required!
throw? result
// throws if result is in error state
Does this proposal fit well with the feel and direction of Swift?
Assuming that try/catch defined the direction: No.
But I do not think that try/catch is a good direction so I think the try-to-result-solution would improve the direction.
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
Given the number of custom Result types in libraries (guilty myself), it obviously is a good thing to have. Most types I had seen do not have anything beyond status (success/error), union { value, error }.
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?