Adding Result II: Unconstrained Boogaloo

This isn't just a question of interoperability. I cannot imagine working on a project that doesn't use a Result type. When a foundational type is that pervasive it really does belong in the standard library. The only reason it isn't there yet is that we haven't settled on a design. This thread feels like it is heading in the right direction and I really hope it leads to an accepted proposal.

6 Likes

This may not match the experience of all Swift programmers, but it closely matches mine. I very often need a Result type. And if not on API boundaries, at least in implementations. I mean that even if there are ways to write an API which does not expose a Result type, this type is very often useful anyway.

Sure. It's the same two basic ideas that inform most of the proposal. First, that's the common spelling of those methods from most of the community implementations, so users will be familiar with it. Second, It's also the spelling users will be familiar with from other types in the standard library, like Optional. Additionally, I think it demonstrates the "bias" the type has towards .success being the "expected" result.

As to typed throws, pretty much all of the discussion I've ever seen about it kept throws as the untyped version, with the typed version being optional. If that's the case, I don't think anything you've mentioned would be breaking, we'd just need a typed-throws version of unwrapped().

Result has a huge amount of potential convenience API, not just including transformers but recovery and value/error accessors. I specifically made this proposal minimal so as to limit the necessary bikeshedding. Once the type is in use I believe convenience API can be standardized, depending on common use. Adding all possible API in one shot seems untenable to me.

If you want, perhaps you can list the API you most commonly use, including what's already in the proposal? It's good to see, if nothing else.

We'll just have to disagree. The semantics of the type isn't established just by the name of the type, but the names of the cases as well, and the strength of the bias, as you put it, seems much stronger with success than value. value would be more appropriate for Optional, but some works even better to establish the semantics of the type.

I was talking about "completeness" as a confusing aspect of Result, which I've never seen in the community. As to the case names, I sometimes confuse Optional's .some for .value. I don't think that's an indictment of the naming, as I think .some is superior to .value for Optional, but just the common confusion of synonyms that happens to everyone.

Sorry if the case name discussion has become defensive on my part, it's just the part of the proposal I thought was settled, given the community versions and previous discussions.

I don't think previous discussions got far enough along to get into details like bike shedding of case names. You should interpret this as a good sign for the chances of acceptance! :wink:

Well I think it would be fair if you could extend the alternative suggestions section of your proposal to mention a few of these suggetions from this thread. This will help the review process, because not all of us read the pitch threads and it will prevent users to rehash the same thing during the review. Furthermore the community can vote their preffered naming scheme.

Fair enough.

That sounds good, I'll add alternate spellings to the proposal soon.

1 Like

One last bit of bikeshedding that might be useful is any additional protocol conformances we may want. I've already included Equatable and Hashable, but I didn't include CustomStringConvertible, CustomDebugStringConvertible (as the output already seems okay) or Codable. SPM adds Codable conformance for their Result, which could be interesting built in. I've never needed to persist a Result, but it seems useful. My only concern is whether there's some standard representation we should use, or whether we should leave it up to the user if there is no standard.

Big +1 from me - I really like where this proposal has landed in terms of the feature set and naming.

2 Likes

Parity with Optional seems like a good starting point to me as far as conformances go. It'd probably be useful for the standard library to standardize the coding behavior of both Optional and Result, but IMO it'd be good to consider that separately, since it involves some amount of coordination and research into existing practice of its own.

5 Likes

To me this is another reason why .some should be the spelling of the success case, and .error for the failure case.

1 Like

+1 on this, whatever the implementation and/or naming. The only thing I really care about is that functorial and monadic semantics are preserved with the transforming methods, whatever their names end up being: I've no strong feelings on map versus mapValue, the only thing I'd add is that I'd love if the bifunctorial nature of Result was made explicit with a bimap method.

map, mapError, flatMap and flatMapError are mandatory, in some form, but I also think that fold is the essential method to add for a type like this: I think that it would make a lot of sense for Optional too.

I agree on adding throwing transforms when the error associated type is Swift.Error: that would play nicely with those who make extensive use of the standard Swift error handling, but would want an actual type to work with. They still shouldn't be added on the transformError methods though, because it would be confusing to be able to return some error both with return and throw.

I'd say that parity with Optional is a good idea: Result is a wrapper type in the same way Optional is

2 Likes

It looks like Optional conforms to CustomDebugStringConvertible and CustomReflectable, in addition to Equatable and Hashable. I'm guessing its Codable conformance is somewhere else? Still synthesized?

CustomDebugStringConvertible seems pretty straightforward, I just modified the Optional implementation:

extension Result : CustomDebugStringConvertible {
  public var debugDescription: String {
    var output = "Result."
    switch self {
    case let .success(value):
      output += "success("
      debugPrint(value, terminator: "", to: &output)
    case let .failure(error):
      output += "failure("
      debugPrint(error, terminator: "", to: &output)
    }
    output += ")"
    
    return output
  }
}

I've never implemented CustomReflectable before, so I really have no idea what it's supposed to do:

extension Result : CustomReflectable {
    public var customMirror: Mirror {
        switch self {
        case let .success(value):
            return Mirror(
                self,
                children: [ "success": value ],
                displayStyle: .optional)
        case let .failure(error):
            return Mirror(
                self,
                children: [ "failure": error ],
                displayStyle: .optional)
        }
    }
}

Additionally, should I add tests for these conformances?

I'm still working on updating the proposal and implementation with the latest suggestions, bug should have new versions up soon.

The Custom* protocols don't change the API of the type. If the default behavior they customize is acceptable (and it seems like it should be), then there's no need to override them.

I think printing a Result is better with the DebugCustomStringConvertible implementation, as otherwise you just get success(#value#), as it matches the Optional behavior a bit better. I have no idea what makes for a good Mirror so I'm okay leaving that one off.

1 Like

I've updated the proposal and implementation with styling changes, a bit about alternate spellings, and a CustomDebugStringConvertible implementation.

I think the alternative spellings could use an expansion on the idea of making Result closely resemble Optional.

enum Result<Wrapped, E> {
    case some(Wrapped)
    case error(E)
}

Where E could be any spelling.

The only reason I bring this up is because .some would be important if we generalize the ? and if let unwrapping to also work with Result types.

2 Likes

Just a quick thought: Would it make sense to add the following conditional conformance?

extension Result : Swift.Error where Error : Swift.Error {}

So a result can be an error with either an error or a default value wrapped in the success case?!

On the other hand I‘m not entirely sure if the where clause is necessary at all.

2 Likes

I don't really see how that would be useful. It wouldn't really make sense to throw a Result.success(...).

The result already contains this, which should be fine for going back from a result to normal Swift error handling:

extension Result where Error: Swift.Error {
  /// Unwraps the `Result` into a throwing expression.
  ///
  /// - Returns: The success value, if the instance is a success.
  /// - Throws:  The error value, if the instance is a failure.
  public func unwrapped() throws -> Value
}

This seems exactly opposite to me. Result is the type that you can implicitly assign to a variable name and ignore, or throw in a queue for reactive use cases. Result is the one where you might flatmap in more logic (which - doesn’t that break with typed errors in results?)

Errors are what require you to always acknowledge if not deal with the error at a syntactic level. You can delegate up, but you aren’t really meant to be deferring an error - you instead are putting a part of the system into an error state. Deferring an error would be... wrapping it into a result

This pitch is now in review, under the SE-0235 name.