[Pitch] Better Result<Success, Failure> Guard Evaluation

Hi all, apologies if I mess something up in this pitch. I've not ever contributed to the Swift forums before.

The introduction of the Result type into the standard language in SE-0235 is one of my favorite features when building libraries. When a user of a returned Result would like to evaluate it... it usually looks a bit like this:

let result:  Result<Data, MyCustomError> = getResult()

switch result {
case .success(let data):
  // Do something with the data here
case .failure(let error):
  // Handle error
}

Which is fine for most cases. However, in the event we need to cascade multiple results, it can get a bit ugly.
For example:

let firstResult:  Result<Data, MyCustomError> = getFirstResult()

switch firstResult {
case .success(let data):
  let secondResult: Result<Data, MyCustomError> = getSecondResult(using: data)
  switch secondResult {
  case .success(let secondData):
    // Do something with secondData
  case .failure(let secondError):
    // Handle secondError
  } 
case .failure(let firstError):
  // Handle firstError
}

Which, as you can see, gets a bit heavy with the nested switching. This often reminds me of nested if statements, and as a “Never Nester” I would typically fall back on the guard keyword here. I think that the guard keyword would be very useful for evaluating Result types too - however as it works right now the guard keyword has a problem with Results…

let firstResult:  Result<Data, MyCustomError> = getFirstResult()
guard case .success(let data) = firstResult else {
  // Handle first error - But no reference to firstError! 
  return
}
 
let secondResult: Result<Data, MyCustomError> = getSecondResult(using: data)
guard case .success(let secondData) = secondResult else {
  // Handle second error - But no reference to secondError! 
  return
} 

As the comments suggest, I don’t have reference to the error type in the else portion of the guard statement. As far as I can tell, there’s no way to have this syntax and have access to the returned error type.

Perhaps there’s some way to have this functionality by either expanding the guard statement or by updating the Result type?

So thats why I’m making this pitch. I’m not 100% sure what the fix is or should be. I’m not even sure if this is a useful change to the language or something that other users of Swift want - but let’s discuss that here!

Thank you for taking the time to read this.

2 Likes

The general solution to your problem is to use try instead of Result.

do {
  let data = try getFirstResult()
  let secondData = try getSecondResult(using: data)
} catch {
  // handle error
}

Doesn’t that read much more nicely?

1 Like

Whoh that is clean. But I suppose it requires the "getResult" functions to throw? If they don't you can use .get() on the result.

do {
  let data = try getFirstResult().get()
  let secondData = try getSecondResult(using: data).get()
} catch {
  // handle error
}

It certainly is much cleaner this way!

1 Like

For the particular case of Result, I agree with Kyle that switching over to error handling is a good alternative. You gave me an idea, though, for what could be a general solution to this sort of problem, where you want to unwrap a series of enums without climbing a "pyramid of doom", while still processing data from the alternate cases on the way. What if we had a guard switch, which behaves like a switch, except every case must either exit the enclosing scope, or else fall into the enclosing scope with a common set of pattern bindings, which become available in the following code:

guard switch firstResult {
case .success(let firstData):
  break // firstData becomes available below
case .failure(let error):
  log(error)
  return // must exit scope
}

let secondResult = getSecondResult(using: firstData)
guard switch secondResult {
case .success(let secondData):
  break // secondData becomes available below
case .failure(let error):
  log(error)
  return // must exit scope
}

process(firstData, with: secondData)

That's still rather bulky for Result, but you could imagine more elaborate application-specific enums, where there is one case that's relevant to the happy path of a function, but the function still wants to be able to react appropriately to values off the happy path:

enum Configuration {
  case good(Any)
  case better(Array<Any>)
  case best(Array<Int>)
  case bad(Error)
}

guard switch configuration {
case .good(let ints as Array<Int>),
     .better(let ints as Array<Int>),
     .best(let ints):
  break // ints become available below

case .good(let somethingElse):
  log("configuration was good, but not good enough: \(somethingElse)")
  return

case .better(let somethingElse):
  log("configuration was better, but still not good enough: \(somethingElse)")
  return

case .bad(let error):
  throw error
}

process(ints)
6 Likes

I think on the Rust forums I saw someone toss out (the equivalent of)

guard case .foo(let a) = b else switch {
  case .bar:
    return
  case .baz:
    throw MyError.bad
}

(in Rust this is idiomatic indentation; hard to say for Swift)

Didn’t get too much traction there, but their guard equivalent is still pretty new.

9 Likes

That looks like a nicer way of separating the "good" continuation cases from the "bad" ones when there's only one continuation case. It would be cool to support alternate "good" cases too, though.

5 Likes

Truly saying would prefer something like this, if guard knows that it works with Result<> type:

let answer: Result<Int, CustomError>
guard success(let resultingInt) = answer else { error in
    print(error)
    return
}

Here you have visually separated the error handling from the rest of the logic. And you're not telling anybody how to treat those errors (with switch or etc.)

If community will like the idea can try to work on implementation and proposal)

As much as I like what else I'm seeing here, the original example is served well by flatMap. Aside from the ugly naming resulting from the inability to refer to outer-scoped variables after they've been shadowed, that is.

let data: Data

switch getFirstResult().flatMap(getSecondResult) {
case .success(let _data): data = _data
case .failure(let error):
  // Handle either of the `MyCustomError`s
  return
}

// `data` is available.

Another option:

guard let data = getFirstResult().flatMap(getSecondResult).get({ error in

})
else { return }
public extension Result {
  /// A version of `get` that allows for processing a strongly-typed error, upon failure.
  func get(_ catch: (Failure) -> Void) -> Success? {
    switch self {
    case .success(let success):
      return success
    case .failure(let failure):
      `catch`(failure)
      return nil
    }
  }
}
4 Likes

There was idle talk in the past of making the first below be sugar for the second:

func foo() throws Foopocalyse -> Bar { ... }
func foo() -> Result<Bar, Foopocalyse> { ... }

IIRC, it ran aground on (1) arguments over whether Swift should have checked error types and (2) what happens when a func both throws and returns Result.

However, the language — at least on the surface — is just so darned close to supporting it, since Swift throws really does behave more like a special return value than an exception (no stack teleportation!), and it always struck me as where the language ought to end up.

It would solve the problem at hand.

5 Likes

Really like that train of thought.

What is that?

Perhaps the following would be reasonable in such a case (not showing Error argument for brevity):

func foo() throws -> Result<Bar> { ... }
<–––>
func foo() -> Result<Result<Bar>> { ... }

Note that you can wrap a throwing function call in Result { … } to get a Result<ReturnValue, Error> from it, like so:

func funcThatThrows() throws -> Int {
    // ...
}

let result = Result { try funcThatThrows() }
// => result: Result<Int, Error>
1 Like