Add value property to Result

With typed throws, Result will be functionally no different than its own get method, aside from parentheses. The get method is the success's get accessor—it's a method because accessors are not available in Swift as closures. A type that is synonymous with one of its properties (proposed here as value) is a property wrapper.

struct S {
  struct Error: Swift.Error { }

  @Result(Error.self) var value = 1

  mutating func reassignValue() {
    let one = try? value
    value = 2
    $value = .failure(.init())
  }
}
Compiling but not useful implementation for that
@propertyWrapper public enum Result<Success, Failure: Error> {
  case success(Success)
  case failure(Failure)

  public var wrappedValue: Success {
    get { // `throws` belongs here.
      switch self {
      case let .success(success):
        return success
      case let .failure(failure):
        fatalError() // `throw failure` doesn't work yet.
      }
    }
    set { self = .success(newValue) }
  }

  public var projectedValue: Self {
    get { self }
    set { self = newValue }
  }
}

public extension Result {
  init(wrappedValue: Success, _: Failure.Type) {
    self = .success(wrappedValue)
  }
}

I see your point, but I don't think this behavior makes sense as a property wrapper. A Result is a value, not a set of behaviors. To make it so blurs the line too much I think. I do think such equivalence could be useful in some scenarios, I just don't think property wrappers are the way to go. And I don't think such evolution is blocked by this proposal.

3 Likes
  1. If typed throws had existed in Swift 1, we would not have both Optional and Result. They're the same thing (two caches, instead of a "get" closure for both), separated only because of still-missing features.
  2. Optional would have been a property wrapper instead of compiler magic.
1 Like

I see no reason why either of those would be true.

9 Likes

+1 this is great. I agree that .value is a good name for this. see below

-Chris

1 Like

I second with @Jon_Shier here in all the points. I also don‘t see how typed throws would have eliminated the need for Result and Optional in the early days as those types serve different needs (optionality is not always an error but just the complier guaranteed absence of a value).

I hope the core team considered the situation in the future iff we get typed throws that there will be a mechanism to make the throws effect which today returns only Error, to return Failure instead.

If the above is not possible, then the current shift from get() to value is being made too early as we‘d burn the opportunity to expose the correct Failure type.

8 Likes

Aha, yes, I missed that. I agree that the right type for this should be:

extension Result {
  public var value: Success {
    get throws Failure {   // typed throw here.

I don't see how we could retroactively fix this with an addition of typed throws in the future (without a major source break) so waiting for that seems pragmatic.

-Chris

16 Likes

Yep, I agree we should wait for the resolution of typed throws.

4 Likes

Counterpoint: typed throws is actively harmful at the public-API level, and we should not delay improvements to the standard library in order to accommodate a hypothetical future anti-pattern.

3 Likes

While I can see the argument that using typed throws in public methods may not be advisable (anti pattern seems hyperbolic), this use on Result should be largely uncontroversial given it exposes type information that already exists. If the user wants to erase the error there they can simply erase the Result.

8 Likes

I cite previous forum threads about typed throws:

(The entirety of that comment by John McCall is worth reading.)

4 Likes

I'd disagree with most of those assessments, but that doesn't seem particularly relevant to the discussion at hand: the proposed value property isn't ideal given a possible typed throws feature. So unless the core team has ruled that feature out entirely then it shouldn't be added. The merits of such a feature are a separate discussion. This is similar to how a possible synthesized associated value accessors feature blocks the addition of success and failure properties.

5 Likes

While explicit error types might be harmful in some situations, it's a library design flaw and should be rather handled by some design guidelines on a case by case basis, not a language constraint that should exist. If you don't like it, don't use Result or erase manually into the Error only world. Erasing the Failure type from Result is and was a language limitation just like the previously missing throwing effect. It feels conterproductive to introduce Result with an explicit Failure, only then to keep erasing the type.

4 Likes

This pitch is intended to make Result and Task consistent, so we should try to leave room for typed throws in both APIs.


Task.init and Task.detached are only available where Failure == Error and Failure == Never.

Can a Failure == Error constraint also be added to the existing Task.value property?

 @available(SwiftStdlib 5.5, *)
-extension Task {
+extension Task where Failure == Error {
   public var value: Success { get async throws }
 }

 @available(SwiftStdlib 5.5, *)
 extension Task where Failure == Never {
   public var value: Success { get async }
 }

Will it then be possible to add an unconstrained Task.value property — with typed throws — in the future?

+@available(SwiftStdlib 6.0, *)
+extension Task {
+  public var value: Success { get async throws(Failure) }
+}

Can the new Result.value property also be constrained where Failure == Error and Failure == Never?

13 Likes

Just to modify my endorsement above, I hadn't thought of the evolution consequences on typed throws. If that is an issue, I too would prefer to defer it.

The reason I didn't consider this originally is my brain had assumed that on source compatibility terms, making an API declaration type-specific on throws would be forward source compatible. As in, throws Error — the default today — is a supertype of throws Failure so today's do { ... } catch {} would continue to work, if the value declaration was later changed to only throw a specific error.

I guess the issue posed up-thread is relating to binary compatibility?

1 Like

I think we should get a clear signal from the core team about specific issues of typed throws. It seems odd to me that we hold every proposal hostage (that include throws) to a future requirement/proposal that has not been accepted.

In the below post it seems to me that there may be a solution in the future that does not require us to deploy a pre-emptive support for type throws (I may be wrong?)

The proposal as-is seems fine to me +1 (even thought I am still trying to get over the fact that properties can now throw)

1 Like

I agree, in general, that we should not hold proposals hostage to a possible future proposal. However, the lack of approval of the proposal in question is not limiting at all, since users can still use get().

i use this in my sources:

extension Result {
    var success: Success? {
        switch self {
        case .success(let v): return v
        case .failure: return nil
        }
    }
    var failure: Failure? {
        switch self {
        case .success: return nil
        case .failure(let v): return v
        }
    }
}

would be great to see it built-in.

Proposals are deferred, in part or in whole, on a regular basis when the core team feels that future work may unlock a desired change in a better way. This happened in the original Result proposal with the removal of the success and failure properties pending a "generated associated value accessors" feature for enums.

3 Likes