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

I don't want to sound impatient, but can the core team clarify if we can retrofit the revised implementation after this last review into the Swift 5 branch, or is it too much of a hassle?

I think there's a good shot that this will make Swift 5 if it's accepted, but I don't want to promise that.

Also since this proposal is coming hands in hands with the very first self-conforming protocol ( Error ) can you please officially clarify the ultimate long term goal we want to reach here?!

It's not the first self-conforming protocol; @objc protocols have long been able to self-conform. But yes, I think we'd like to eventually extend self-conformance in some way to a broader set of protocols, and similarly we'd like to allow protocol compositions to conform to their implied protocols. I'm not going to promise any particular timeline for adding that support, though.

Also, depending on the exact rules, self-conformance for arbitrary protocols may be evolution-limiting for that protocol: it would (probably?) become illegal to add requirements to a protocol that would prevent self-conformance (like an initializer, a static method, or a contravariant use of a Self-dependent type) and thus invalidate extant uses of the self-conformance. That's a limitation which we're already accepting for @objc protocols, but I don't know if we'd want to accept it by default for an arbitrary protocol, so self-conformance might require explicit opt-in.

6 Likes

I agree with this. Because although this expression is likely to primarily exist inside a process producing a Result rather than at the usage location of the result (and thus be mostly hidden away), that is precisely where I would want the expressivity that .success/.failure provide. It gives finality to the code paths that simply .value/.error do not offer.

3 Likes

Thank you John, that's all fine by me. I think I can fairly say that the provided information are very valuable to everyone who is following this thread, since this shares a little more of the Swifts future envisioned by the core team.

I have been a proponent of making wrapper types easier to work with specifically in the context of typed errors for a long time. Rust has some macros that really help in this area. If you look up the old typed errors threads you will find discussion of this topic.

I agree with the above, and would also prefer to avoid the constraint; instead type constrain just the implementation of unwrapped..

extension Result where Error == Swift.Error {
  public func unwrapped() throws -> Value
}
5 Likes

Continuing some of my thoughts, the names .value and .error seem to convey to me the idea that a Result's purpose is to hold either a value, or an error in absense of a value. And this feels too similar to Optional to me, even with the associated value on .error.

The Result pattern gained traction largely from its convenient abstraction of optionals, but I believe a valuable secondary distinction is in its ability to say more than simply 'this or that' -- it says explicitly whether the task succeeded or failed. This can make code that uses it so much clearer, and I'd hate to see that lost.

1 Like

What exactly is the difference between the map and flatMap (or their Error-oriented cousins) methods?

Assume the Result is the result of an initial, failable operation.

  • map is for transforming the result if the first operation succeeded.
  • mapError is for transforming the error if the first operation failed.
  • flatMap is for chaining a successful result from the first operation into a second failable operation.
  • flatMapError is for attempting to recover from a failure of the first operation.

In any case, the idea is that you get a result and just stack up a bunch of maps and flatMaps on it without ever doing explicit control flow.

3 Likes

+1 from me on this. I am very happy to see typed errors, as well as Error: Error.

Suggested refinements look reasonable to me, except I would prefer not to change the cases names from success/failure to value/error. I think and agree with others that success/failure better convey the semantics of the type, as was argued numerous times so far.

The only argument given for the case name change is the misalignment with the generic type name. Is that really a problem? Does that alignment outweigh semantic clarity of code? If so, maybe we could rename the generic type to something like SuccessValue or Wrapped rather than renaming case names as suggested. Generic type name is not something users will use frequently in their code, while cases names certainly are.

3 Likes

As I mentioned before, it is weird to name a throwing function with the ed ending. In my head that sounds like it should always return or be a computed property. It sounds much better as unwrapping() as in "try unwrapping" vs try unwrapped . The name unwrapping is not excellent either, it is obvious that I am getting a wrapped value back here?

let someWrapped = try? someResult.unwrapped()

At least unwrapping() sounds like it is going to try to unwrap vs unwrapped() which sounds like it unwrapped something when it some cases it will not since there is only an error. Are we unwrapping errors now? Me mental model needs to realign. Another option could be:

try someResult.gettingValue()

I think the naming in the cases are a little unfortunate because they do not align with anything that I am familiar with. I would be more happy is this aligned with Optional's .some naming for the success case. Just the word value seems off to me when I think about value semantics and reference semantics. I've got to admit that I am not against the current proposed naming, I like it better than the success/failure pair but I much rather with something like .okay / .error or .okay / .err. Err is a word and rust uses it, so why not?

1 Like

But function names don't normally have "ing" endings, even when they can throw (e.g. try data.write(to: url), not try data.writing(to: url)). If you want it to sound like an action, using the imperative form (unwrap) would be the way to go.

(I'm not saying unwrap is better than unwrapped or vice versa, just that unwrapping is worse.)

"err" is a verb; verbs are usually used as names of functions. (It's also an intransitive verb, making the construction syntax Result.err(someError) especially strange.)

We also just never do that kind of name-shortening in the library. Other languages do that a lot, but it's explicitly not okay under the Swift guidelines.

9 Likes

I would also prefer success/failure than value/error. value(value) seems redundant and confusing to me, and it also gives me an impression that the error case doesn't carry a value (though it does carry an error value).

success/failure also reads more natural. We would say "our efforts resulted in success/failure", but we wouldn't say "our efforts resulted in value/error".

4 Likes

+1

I appreciate the revisions and agree:

  • Switching the case names to value and error plays nicely with potential future enum ergonomics.

  • While I like fold, I understand that language-level sugar around switch would be better across the board.

  • The map/flatMap/mapError/flatMapError naming is good. There's no need to mapValue and flatMapValue when there's an obvious bias toward the happy path. The existence of mapValues and compactMapValues on Dictionary<Key, Value> adds clarity to the fact that both Key and Value exist at the same time and the name map could imply the ability to transform both.

  • The removals all make sense to me.

While I don't love the Swift.Error constraint, I understand it. I'll continue to define Either in my projects as a result :slight_smile:

4 Likes

So you should be using an Optional Error instead. There is no point using Result is the result has no value.

I greatly prefer value and error. I find it far more clear to talk about the Result Value than the Result success.

And I don't find value redundant, as I rarely call my variable result. In a switch case, I would rather use .value(response), .value(total), or any other name that is sensible in the context.

1 Like

Is it possible to provide examples where map and flatMap (and mapError vs. flatMapError) differ?

I apologise for not participating in the original thread, but I have essentially been ghosting the forum as a reader and have read the arguments.

I think the revised proposal is great, acknowledges the need for Result in light of future error handling with async/await stuff, and proposes a sensible type API.

I would prefer if the case base names were success (or even succeeded) and failure (or failed). I think if you are making an opinionated enum type, the case names should be semantically meaningful as much as anything else. value/error is very neutral. Switching on the latter would result in a lot of duplicated words too.

I know there is an allusion here that the case names will eventually become synthesised optional properties like var value: Value? but I think this shows the limitations in applying automatic synthesis everywhere. I would hope any future enum property synthesis is opt-in and at that time we would provide the custom var value: Value? formulation rather than var success: Value?.

11 Likes

An enthusiastic +1 to everything @bzamayo just said. He captured my thoughts exactly.

I generally agree. At the same time, the ability to generalize over all sorts of results has utility. For example, PromiseKit makes good use of empty results, and maybe the case of async calls should be considered when estimating the usefulness of Void result values.

3 Likes