[Revised] SE-0235: Add Result to the Standard Library

(David Owens II) #102

I prefer this iteration over the last. However, I don’t like the bias towards value over error in the map and flatMap fuctions. I think this subtely perpetuates a general issue in software development of ignoring the error code paths or treating them as secondary conditions to worry about.

I would prefer named parameters:

  • map<NewValue>(value transform: (Value) -> NewValue) -> Result<NewValue, Error>
  • map<NewError>(error transform: (Error) -> NewError) -> Result<Value, NewError>

Also, there are errors in the comments referring to old names of the values.

I’m also a little confused as to why there isn’t a map that transforms to Result<NewValue, NewError>.

If I’m propogating type conversions and errors from one layer to another layer (for instance, converting string values to typed representations from a config file - not the example code below, but a real use case), I don’t want to write this:

let mr4: Result<Double, MyError> = r1
    .map({ return Double($0) })
    .mapError({ _ in return MyError() })

I want to write this:

let mr3: Result<Double, MyError> = r1.map {
    switch $0 {
    case let .value(v): return .value(Double(v))
    case let .error(e): return .error(MyError())

Or with a library function that I wrote to handle this:

func transformer(result: Result<Int, String>) -> Result<Double, MyError> {
    switch result {
    case let .value(v): return .value(Double(v))
    case let .error(e): return .error(MyError())

let mr5 = r1.map(transformer)

(Sebastian) #103

+1 On the reasoning for the status/value distinction.

I want to add that "success" and "failure" feel a bit a) too strong and b) over-specific to me, as not every call can fail or succeed in the narrower sense. "Result" is an appropriately general term that can encompass more than just actions that have to either fail or succeed. When you retrieve data, you expect a result value, not a great "success".

Edit: I think I completely misunderstood your point and now I'm confused. I agree to the status/value distinction but that leads me to conclude that "value" is a better case name than "success". A result hints to some level of success (its status might be "successful") if it carries a value.

(Nick Lockwood) #104

The guidance is not to conform stdlib types to other stdlib types, but I would guess it's OK to do:

extension Array : Error where Element == MyError {}

Because although Array and Error are stdlib types, MyError isnt. Using tuples would be more problematic since you can't conform non-nominal types to protocols, but creating a struct seems like a reasonable workaround.

(Jordan Rose) #105

Named parameters would forbid the use of trailing closures, which I at least would be unhappy about.

The general map you wrote isn't a map; it's just a function call. transformer(r1) has the same result.

(Jordan Rose) #106

It's not okay because a type can only conform to a protocol in one way in Swift.

(Nick Lockwood) #107

I wondered that too, but I assume it's because it doesn't offer much value in terms of syntactic sugar. map and mapError allow you to write code like:

let mr: Result<Double, MyError> = r1.map { Double($0) }

i.e. to convert an Int Result to Double Result without needing to explicitly unwrap and switch over the Result. But a hypothetical map variant that both received and returned a Result would require the implementer to use switch, as per your own example:

let mr3: Result<Double, MyError> = r1.map {
    switch $0 {
    case let .value(v): return .value(Double(v))
    case let .error(e): return .error(MyError())

The thing is, you can already do that with almost the same code without needing map:

let mr3: Result<Double, MyError> = {
    switch $0 {
    case let .value(v): return .value(Double(v))
    case let .error(e): return .error(MyError())

It seems to me that the purpose of map is to apply the same closure for each item in a collection (which, in the case of Optional or Result means applying it either zero or one time since they are both effectively a collection of either one or zero items).

But what you are proposing would be a map that applies a closure once to the whole collection, which then has to enumerate all the cases inside the closure itself.

(Nick Lockwood) #108

Ah, I see.

(Daryle Walker) #109

That would be the same as the fold method in the previous version.

(David Owens II) #110

That's not quite what I was saying... in my example, which was a bit too terse, some of the context was lost.

I have a library lib1. It contains f() that returns Result<SomeValue, SomeError>.
I have another library lib2 and it contains g() that calls f(), however, it doesn't handle any errors here from f(), but propagates those up to the consumers of lib2. However, as we don't want to expose the types from lib1, these are converted to types within lib2, so we have Result<AnotherValue, AnotherError>.

So in g(), I either have this code:

return lib1()
    .map({ return AnotherValue($0) /* subst w/ real conversion code */ })
    .mapError({ _ in return AnotherError() /* subst w/ real conversion code */ })

Or have a function that does this:

return transform(lib1())

Anyhow... I guess I don't see the value in the map/mapError and flatMap/flatMapError functions currently. That's ok though.

(Matthew Johnson) #111

Not exactly, fold took two closures one taking Value and one taking Error, both of which must return the same T.

(Daryle Walker) #112

And said "same T" could be a Result<NewValue, NewError>. The closure for Value would return a new Result that uses its value case and the closure for Error would return a new Result that uses its error case.

(Matthew Johnson) #113

Well if you want to map the value and error separately then what you really want is bimap, not fold. Of course you could define bimap in terms of fold but they are not the same. They are also both different than the method @owensd posted because that takes Result as input, not Value or Error.

(Riley Testut) #114

FWIW, I also used to believe a Result with no value would be essentially pointless when you could just use an optional error. However, in the project I'm working on now, I'm chaining together several network requests/database operations and using generic code to handle their results. While most requests do return a (meaningful) value, some don't, and I just need to know whether the request succeeded or failed with an error. Without support for Result<Void>, I wouldn't be able to generalize my logic across all requests, which would make my code far more complex and repetitive.

This use case is actually the biggest reason why I'm very opposed to renaming the cases from .success/.failure to .value/.error. Now, when checking the result of an operation that doesn't return a value, I would need to check that the result's case is .value...even though there is no actual value. I understand the rationale behind renaming the cases, but IMO the symmetry between case names and generic type parameters is a relatively small win, and noticeably worsens the semantics of using Result<Void>. I also hope that whatever solution we design for easily accessing enum associated values as properties is flexible enough that you could have var value: Value? and still have the case name .success. (In addition, I do also believe success/failure are simply better names for Result, since a result is conceptually a success or failure, as others have said. Though, I wouldn't be that opposed to using .success and .error).

As an aside, another convenience I've made for myself is to redeclare my unwrapped() function with @discardableResult in an extension for Result<Void>. This way I can easily unwrap the error simply via try result.unwrapped() instead of _ = try result.unwrapped(), while keeping the unused result warning when the value type is not Void. I would love to see this small improvement added to the standard library's implementation, but at least I could continue to use a parameterized extension if it doesn't.

Overall though, I'm quite happy with the Core Team's revisions to the proposal (very happy Error is constrained to Swift.Error and that Swift.Error will now self-conform), and ultimately would still be happy to finally have Result in the standard library, even if I dislike the case names.

(Jamie Pinkham) #115

I haven't seen any discussion around the variance affordances. Will whatever compiler magic is used to make generic types like Array covariant be used to make Result covariant? Will it be covariant in both types?

(John McCall) #116

As proposed, Result has no subtyping conversions. We can consider adding them later, but I personally expect that async / await will reduce the importance of Result to the point that that sort of affordance wouldn't be justified (at least, for people not using Result as an error-handling replacement, which I have no interest in adding complexity to support).

(Jamie Pinkham) #117

Should the proposal be modified to include either a clarification that it will be invariant, or updated to include covariance? I can see a lot of users of this type being confused as to why their method that takes a Result<Animal, MyError> can't take an instance of Result<Cat, MyError> (given the obvious assumption that Cat is a subtype of Animal). Without that covariance in at least the Value position, it would cause users to have to write awkward code like func doSomethingWithAnimalResult(catResult.map { $0 as Animal })

Ideally, it should be covariant in both positions, like Scala's equivalent Either type.

(John McCall) #118

I certainly agree that it would be more convenient to have covariant conversions. The proposal doesn't include them because — while we believe that Result will still be useful after we've added async / await — we don't believe it will still be useful enough to justify the complexity cost of adding those conversions. That is, it will no longer be important for working with Result to be especially convenient.

If you believe that Result should not be added without such conversions, you should recommend rejecting this proposal.

(Jamie Pinkham) #119

I'm a -1 on this proposal, specifically regarding the lack of addressing variance in the types, based on this conversation: [Revised] SE-0235: Add Result to the Standard Library. If we can get support from the Swift team that Result will be covariant in both the value and error position, then I would remove my objection.

(Xiaodi Wu) #120

While I think it’s fine to have this proposal without it for now, I do think having covariance for both Value and Error would be invaluable, both in terms of usability and learnability, but also because I think we’d like to avoid erecting barriers between Result<T, MyError> and Result<T, Error>, and I hope that it can be a later addition to Swift.

(Jordan Rose) #121

I think there are a lot of types where variance would be valuable, and that Result is not sufficiently special enough to prompt the extra compiler and runtime work here. I'd like to some day have a general variance story but it is definitely difficult in the general case.