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

+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

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)

+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.

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.

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.

3 Likes

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

2 Likes

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.

Ah, I see.

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