[Pitch] Result success and failure accessors

Hi folks!

This is a bit inspired by Rust's unwrap_or / unwrap_or_else methods.

The idea is having success(or:) / failure(or:)methods in Swift.Result to allow to lift a success or failure without running into throwing errors like exists today in get() or having to do a pattern match.

func returnsResult() -> Result<Int, any Error> {}

// current matching
func returnsDefaultValue() -> Int {
  guard case let .success(success) = returnsResult() else { return 1 }
  return success
}

// or even with a try?
func returnsDefaultValue() -> Int {
  try? returnsValue().get() ?? 1
}

// would become instead
func returnsDefaultValue() {
  returnsResult().success(or: 1)
}

The obvious advantage is in how you read this when you have multiple calls that return Result

async let request1 = service.request1().get()
async let request2 = service.request2().get()

let (response1, response2) = (
  (try? await request1) ?? 1,
  (try? await request2) ?? 2
)

// would become instead

async let request1 = service.request1().success(or: 1)
async let request2 = service.request2().success(or: 2)

let (response1, response2) = await (request1, request2)

Below is a quick draft in how I'm thinking:

extension Result {
  /// Returns the success value contained in the `Result` enum or the value generated by the closure in case of failure
  /// - Parameter otherwise: A closure that generates a value to be returned in case `Result` is a failure
  /// - Returns:The success value present in `Result` or the value generated by the closure in case `Result` is a failure
  @_alwaysEmitIntoClient
  func success(or otherwise: () -> Success) -> Success {
    guard case let .success(success) = self else { return otherwise() }
    return success
  }

  /// Returns the success value contained in the `Result` enum or the value passed to the function
  /// - Parameter otherwise: The  value to be returned in case `Result` is a failure
  /// - Returns:The success value present in `Result` or the value passed to the function in case `Result` is a failure
  @_alwaysEmitIntoClient
  func success(or otherwise: Success) -> Success {
    success(or: { otherwise })
  }
}

extension Result {
  /// Returns the failure value contained in the `Result` enum or the value generated by the closure in case of success
  /// - Parameter otherwise: A closure that generates a value to be returned in case `Result` is a success
  /// - Returns:The failure value present in `Result` or the value generated by the closure in case `Result` is a success
  @_alwaysEmitIntoClient
  func failure(or otherwise: () -> Failure) -> Failure {
    guard case let .failure(failure) = self else { return otherwise() }
    return failure
  }

  /// Returns the failure value contained in the `Result` enum or the value passed to the function
  /// - Parameter otherwise: The  value to be returned in case `Result` is a success
  /// - Returns:The failure value present in `Result` or the value passed to the function in case `Result` is a success
  @_alwaysEmitIntoClient
  func failure(or otherwise: Failure) -> Failure {
    failure(or: { otherwise })
  }
}

extension Result {
  /// Immediately returns the success value contained in the `Result` enum or awaits the value generated by the closure in case of failure
  /// - Parameter otherwise: A closure that generates a value to be returned in case `Result` is a failure
  /// - Returns:The success value present in `Result` or the value generated by the closure in case `Result` is a failure
  @_alwaysEmitIntoClient
  func success(or otherwise: () async -> Success) async -> Success {
    guard case let .success(success) = self else { return await otherwise() }
    return success
  }

  /// Immediately returns the failure value contained in the `Result` enum or awaits the value generated by the closure in case of success
  /// - Parameter otherwise: A closure that generates a value to be returned in case `Result` is a success
  /// - Returns:The failure value present in `Result` or the value generated by the closure in case `Result` is a success
  @_alwaysEmitIntoClient
  func failure(or otherwise: () async -> Failure) async -> Failure {
    guard case let .failure(failure) = self else { return await otherwise() }
    return failure
  }
}

Not convincing, as

  1. You constructed the examples to not use the options similarly, and they look too close to bother when that is done:
let (response1, response2) = await (
  (try? request1) ?? 1,
  (try? request2) ?? 2
)
let (response1, response2) = await (
  request1.success(or: 1),
  request2.success(or: 2)
)

async let request1 = (try? service.request1().get()) ?? 1
async let request2 = (try? service.request2().get()) ?? 2
async let request1 = service.request1().success(or: 1)
async let request2 = service.request2().success(or: 2)
  1. No effort should be put here because typed throws solved the problem better.
async let request1 = (try? service.request1()) ?? 1
async let request2 = (try? service.request2()) ?? 2

Fair point, I actually haven't thought about writing like this.

I think it's just a bit more expressive than lifting it into an optional and then using the ?? operator.

Perhaps this can evolve into Result also supporting the ?? operator or having some sort of syntactic sugar to lift typed throw functions into Result types?

The main motivation is also for chains of transformation.
Take in consideration something like this:

// given func request() async -> Result<MyType, any Error>

await request()
  .map(MyOtherType.self)
  .mapError(MyError.self)
  .success(or: MyOtherType())

// versus current

try? await request()
  .map(MyOtherType.self)
  .mapError(MyError.self)
  .get() ?? MyOtherType() 

I feel it's a bit confusing to have the try? there because the addition of .get(). Ofc if the community feels this is not needed, then happy to pull the idea out.

There's also the possibility in that design of having something similar to Rust's unwrap_or_else as in you pass a closure to the success(or:) function

1 Like

That won't happen, but I wish it would.

public extension Result where Success: ~Copyable {
  @inlinable init(catching body: () async throws(Failure) -> Success) async {
    do { self = .success(try await body()) }
    catch { self = .failure(error) }
  }
}

That doesn't compile. It looks like

(try? MyOtherType(await request().get())) ?? .init()
(try? await request().get()).map(MyOtherType.init) ?? .init()
struct MyOtherType {
  init(_: MyType) { }
  init() { }
}

…which, like many other problems, would be best served with do-catch expressions:

do { MyOtherType(try await request()) }
catch { .init() }
func request() async throws(MyError) -> MyType {
1 Like

Also, even if we were going to add something like this, I think your otherwise parameters should probably be auto-closures.

You need an ugly overload specifically to support typed errors, while @autoclosure is still buggy with them. The non-throwing overload is good though.

public extension Result {
  @inlinable static func ?? <Error>(
    result: Self,
    defaultValue: () throws(Error) -> Success
  ) throws(Error) -> Success {
    do { return try result.get() }
    catch { return try defaultValue() }
  }

  @inlinable static func ?? (
    result: Self,
    defaultValue: @autoclosure () -> Success
  ) -> Success {
    do { return try result.get() }
    catch { return defaultValue() }
  }
}
struct Failure: Error { }
let result = Result<Never, _>.failure(Failure())
var error: Never {
  get throws(Failure) { throw .init() }
}
#expect(throws: Failure.self) {
  try result ?? { try error }
}

I like this, but playing devil's advocate here, I feel it might be semantically confusing to have the operator for errors in that case. .success(or:) feels semantically more clear about what's happening.