Adding Result II: Unconstrained Boogaloo

If the second generic parameter is not constrained to Error, then this isn’t really Result, just a mis-named Either.

7 Likes

Compared to Either, Result still makes the "bias" clear as to what's considered a good vs. bad result, even without the Error constraint.

12 Likes

Imho naming is the big flaw of Either: No matter how you call the cases, they are not supposed to convey any meaning.
As it is extremely easy to declare a type with Either-semantic and meaningful names, I don't think there's any benefit in having such an arbitrary enum.
Result, on the other hand, has a clear meaning, and constraining the failure-case just limits what you can express (or should express - as you can make every type conform to Error with a single line of code).
Optional<T>, for example, is more or less equivalent to Result<T, Void>, and you can think of other cases where something might not work as expected, and where you don't care for the reason of the failure, but rather want to provide other information:

  • A failed upload could tell you how many bytes have been transferred, or how you can resume the process
  • SortedArray.insert(element: Element, position: Int) -> Result<Void, Int> could simply be successful when the requested index is valid, and return the actual position if your hint was wrong
  • Even the somewhat odd Result<Void, Void> could be useful as an explicit alternative for returning Bool

Such examples might be considered as antipatterns and misuse of Result, but imho it's ok when this type is more than an asynchronous try/catch.

3 Likes

Quick questions about the 'potential' typed throw future of Swift with the compatibility to the proposed type (I'd really appreciate if also someone from the core team could provide some bikeshedding answer to this questions):

If we had typed throws, would it be possible to upgrade the following method in a non-breaking fashion or would we require a separat method?

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

I think that making it to something like this, would be a breaking change, am I right?

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

Also the above extension is missing an init which is not expressible until there are typed throws in Swift:

extension Result where Error : Swift.Error {
  public init(_ throwing: () throws(Error) -> Value)
}

I'm totally fine with the following extension which is unconstrained to a specific Error type, but the method above feels like it should obey typed throws in the future.

extension Result where Error == Swift.Error {  
  public init(_ throwing: () throws -> Value)
  public func unwrapped() throws -> Value
}

If Error would be Never then I'd expect that the unwrapped method would not require try in a typed throwing system.

That said, I personally think that method related to my question should be renamed to signal that the Error type is statically lost.

Bikeshedding:

extension Result where Error: Swift.Error {
  public func unconstrainedUnwrapped() throws -> Value
}

@Jon_Shier could you provide your reasoning for going with the map and flatMap naming scheme for transforming the value of the result instead of mapValue and flatMapValue which is aligned with mapError and flatMapError, plus it does not burn the methods which 'potentially' can have a generalized meaning in the future of Swift. Sure the my main point here is the word 'potentially', but I'd like to carefully choose the naming scheme which is fair and has a bigger long-term picture in mind.


I know the API surface growed during the discussion therefore the following is just a question if the inclusion on the following methods is needed in the proposal (or at all)!?

extension Result {
  public func materialized() -> Result<Result<Value, Error>, Never> {
    return .success(self)
  }
}

// In the future with parameterized extensions we could do this:
extension<V, E> Result where Value == Result<V, E>, Error == Never {
  public func dematerialized() -> Result<V, E>
}

// The `dematerialized` can be worked around today as well:
extension Result where Error == Never {
  public func dematerialized<V, E>() -> Result<V, E> where Value == Result<V, E> {
    switch self {
    case .success(let value):
      return value
    }
  }
}
// These are also very useful when you need to align some types.
extension Result where Value == Never {
  public func promoteValue<NewValue>(_: NewValue.Type) -> Result<NewValue, Error> {
    switch self {
    case .failure(let error):
      return .failure(error)
    }
  }
}

extension Result where Error == Never {
  public func promoteError<NewError>(_: NewError.Type) -> Result<Value, NewError> {
    switch self {
    case .success(let value):
      return .success(value)
    }
  }
}

Actually, in my experience (and I believe conventionally) it is thought of exactly like an Optional, but with an error payload to describe the failure case. So it really does "wrap" a value. I'm not arguing for the Wrapped and some names that others have but the analogy clearly exists and is very reasonable.

I very strongly disagree with this. value is a perfectly good case name when paired with error. As noted above, the bias is implicit in the semantics of Result. If this pitch were for Either that would not be the case, but we're not talking about Either, we're talking about Result.

I think this is the strongest argument in favor of the spelling you prefer.

This is the spelling I prefer, but I do agree with @Jon_Shier that Result itself lightly implies completeness. I'm not opposed to success and failure on the grounds of implying completeness. I just have trouble remembering what the case names are when I work in projects that use these case names and never have trouble when working in projects that use value and error. This is a sample size of one and may not generalize, but if I have trouble remembering them I imagine others might as well.

Again, only a sample size of one, but I have mentioned in this thread that I find these case names hard to remember. This isn't a significant enough problem for me to have ever complained about it before. It's easy enough to look up the case names when necessary.

I think a bikeshed discussion on the names is an appropriate (and inevitable) part of the SE process. Of course as the author, you should certainly put forward the names you prefer. :slight_smile:

6 Likes

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.