If/guard-let-catch for conveniently accessing both Result type cases

Also, I just want to state this because I've read the word "frustration" a couple of times and I just want to be clear here. I was not frustrated with Swifts error handling at all before writing this thread, so I definitely did not write the proposal out of frustration. Instead I was happy that I found a nice working solution based on Result and this proposal was meant to improve the ergonomics a bit.

Independent of how things go forward with this proposal or typed throws in Swift, I will most probably keep my Result-based solution in my apps because it fulfills my needs the most, and I will also probably write about it, explaining it all in detail to those who are interested. I really don't feel any hatred or frustration, I'm sorry if any of my posts sounded different.

The only annoying thing that frustrates me a little bit now after writing the pitch is the fact that my idea gets rejected by some based on historical discussions about typed throws. I see that as a separate topic though, simply because I'm just trying to improve an already existing feature that already can be used as an alternative to error handling using throws. I do not know the history of how and why the Result feature was introduced, I'm just trying to use the best tools I have available the best way I can for my needs and encountered an inconvenience that I tried to solve. I am a bit surprised that this seems to raise the tempers. I did not intend to do that. And this being my first pitch, it's a bit demoralizing, too. Things like "you're fighting the language, and suffer the bad consequences of this bad decision" when I'm just using built-in Swift features in a way that I actually think is pretty smart...

3 Likes

I apologize if my answers were demoralizing, and for my poor choice of words. Thank you for your detailed expression of your problems with untyped errors. I still think that this was a necessary step for this discussion.

3 Likes

I'm sympathetic to your goal of writing type-safe error handling code, with simpler syntax, but I'm not sure if the proposed syntax is a great choice. We already use if let and guard let to unwrap Optional values. Reusing the same syntax for enums other than Optional seems confusing to me.

if let value = maybeValue { ... }

… is already sugar for

if case .some(let value) = maybeValue { ... }

You propose it also becomes sugar for

if case .success(let value) = maybeValue { ... }

Apart from Optional and the various ExpressibleBy...Literal conforming types, I don't think we have much precedence for elevating certain types with their own special syntax. This feels like a piece-meal ad-hoc solution glued onto Result, with the added disadvantage of overloading existing syntax for added confusion. I'm not sure it results in clearer code.

Plus, there are other types where you want to guard on the "happy" path and make some kind of "unwrapping" in the sad path. Like when you guard error == nil else { } and then have to force-unwrap in the else-clause.

I think we either have to agree as a community that we want typed errors, and make typed throws, or create clear, unambiguous and scalable syntax that we can use as a tool in the language, applicable to any "either/or"-types, and usable across the board.

4 Likes

I’m open to a less confusing syntax if people feel if/guard would be confusing. Note though that “just” if/guard without a catch wouldn’t work like in your examples with my suggestions.

That said, I agree that there should be an as general as possible solution to mutually exclusive types. First I considered writing a proposal for any enum with exactly two cases. But then I read somewhere in the forums that an either type was already considered and dropped in favor of Result, basically representing the most useful case for an either type. So I figured making a smaller suggestion is more likely to get accepted. But I’m totally open to get back to that tuple/bi-enum idea. Do you have a suggestion how that could look like? And is it really more likely to be successful?

Would this work for you (if it was possible)?

guard let profile = try profileResult.get() else {
    return .failure(.apiError(error))
}
3 Likes

Yes, the extra “try” and “.get()” are short enough and wouldn’t bother me at all. The important thing for me is that the “error” needs to be precisely typed by the failure type of the Result.

Everybody needs that even though nobody needs typed errors. Like I showed above, without it, what we have gets horrible unless all error handling is performed at one final endpoint.

func sendTheErrorAlong(value: () throws -> Value) throws {
  let value = try value()
  value
func handleBeforeSending(value _value: () throws -> Value) throws {
  let value: Value

  do {
    value = try _value()
  } catch {
    Logger().error("\(error)")
    throw error
  }

  value

There's nothing in the standard library to help with it, e.g.

let value = try `do`(value) { Logger().error("\($0)") }
public func `do`<Success>(
  _ success: () throws -> Success,
  catch: (any Error) -> Void
) throws -> Success {
  do {
    return try success()
  } catch {
    `catch`(error)
    throw error
  }
}

I'm currently under the impression that a guard/catch statement is all that's necessary to fix the problem, and a do let wouldn't offer any more functionality.

Have you tried mapping the results? It seems to work best if the errors are mapped first.

func loadData() -> Result<Void, ScreenError> {
  return fetchProfile().mapError {
    ScreenError.profileError($0)
  }.flatMap { profile in
    self.name = profile.name
    return fetchImage(url: profile.imageUrl).mapError {
      ScreenError.imageError($0)
    }.map { image in
      self.profileImage = image
    }
  }
}

(Each fetch method uses a different error type, to see how that would work.)

You could also try using custom APIs with multiple trailing closures.

func loadData() -> Result<Void, ScreenError> {
  return fetchProfile().flatMap { profile in
    self.name = profile.name
    return fetchImage(url: profile.imageUrl).map { image in
      self.profileImage = image
    } mapError: {
      ScreenError.imageError($0)
    }
  } mapError: {
    ScreenError.profileError($0)
  }
}
extension Result {

  func map<NewSuccess, NewFailure: Error>(
    _ newSuccess: (Success) -> NewSuccess,
    mapError newFailure: (Failure) -> NewFailure
  ) -> Result<NewSuccess, NewFailure> {
    mapError(newFailure).map(newSuccess)
  }

  func flatMap<NewSuccess, NewFailure: Error>(
    _ newSuccess: (Success) -> Result<NewSuccess, NewFailure>,
    mapError newFailure: (Failure) -> NewFailure
  ) -> Result<NewSuccess, NewFailure> {
    mapError(newFailure).flatMap(newSuccess)
  }
}

I haven't used Result in a project, so I'm unsure of this approach for more complex examples.

I like the idea of guard .. catch, however I think it's not enough and the ideal solution would be for compiler to recognise try Result.get() as typed throw and automatically bind the error in catch statement to Failure type held by Result:

let result: Result<T, F> = ..

// Result.Failure is bound to `let error` either implicitly or 
// explicitly using variable declaration following 
// the catch statement.
guard let value = try result.get() catch [let error: F] {
   // ...
}

In cases when the statement is complex, compiler could fallback to generic Swift.Error, so in pseudo language:


let resultA: Result<T, F1> = ..
let resultB: Result<U, F2> = ..

<InferredFailure> 
guard let valueA = try resultA.get(), 
      let valueB = try resultB.get() 
catch [let error: InferredFailure] 
where 
        InferredFailure = F1 == F2 ? F1 : Swift.Error
{
   // ...
}

For any other throwing functions, compiler could continue using Swift.Error, i.e:

func fn() throws -> Value {
   throw MyError()
}

guard let value = try fn() catch [let error: Swift.Error] {
   // ...
}

Same concept applies when unpacking result and calling any other throwing function within the same guard .. catch statement, i.e:

func fn() throws -> Value {
   throw MyError()
}

let result: Result<T, F> = ..

guard let value = try fn(), 
      let otherValue = try result.get() 
catch [let error: Swift.Error] {
   // ...
}

The one could also explore the idea of making try statements act as early return in contexts returning Result<T, F>:

func a() -> Result<T, F1> {
  let value = try b().mapError({ .. }).get()

  // Do something with value ...
}

func b() -> Result<T, F2> {
  // ..
}

which compiler could translate into:

func a() -> Result<T, F1> {
  let value: Value
  do {
      value = try b().mapError({ .. }).get()
  } catch /* let error: F1 */ {
      // Early return
      return .failure(error)
  }

  // Do something with value ...
}

func b() -> Result<T, F2> {
  // ..
}

Edge case here a function that both throws and returns a Result<T, F> which makes little sense to me, hence it could be tackled in a few ways:

  1. Can be made illegal.

  2. Ignore return type and act as a regular throwing function. That should provide backward compatibility with existing code. Compiler perhaps could issue a warning to notify the developer that perhaps they should stick either with exceptions or Result<T, F>.

  3. Different keyword can be used such as unwrap instead of try to avoid collisions with exceptions, which could be made available only if the function returns Result<T, F> and act in similar fashion as early return ? operator in Rust. But that would be the first class support for Result which the core team seem to have some issues with for reasons that are unknown to me.

    func fn() -> Result<Int, FailureA> {
       let resultA: Result<Int, FailureA> = ..
       let resultB: Result<Int, FailureB> = ..
    
       // returns .failure(error) if resultA contains failure,
       // otherwise assigns valueA and continues execution.
       let valueA = unwrap resultA
    
       // returns .failure(error) if resultB contains failure,
       // otherwise assigns valueB and continues execution.
       let valueB = unwrap resultB.mapError { /* map to FailureA */ }
    
       // Executes when both valueA and valueB are available.
       return .success(valueA + valueB)
    }
    

    Compiler could generate all the boilerplate that we complain about here, such as match success, failure branches and do the implicit return. This approach also does not require re-engineering the guard statement as they become unnecessary but it would require the introduction of a new keyword specifically for Result<T, F> type. In my opinion this is a lesser evil than bending the existing exception mechanism to work for Result<T, F>.

Either of the suggestions above would enable writing concise and strictly typed, yet powerful code with nice early returns, while also providing ability to map returned errors before returning them. However someone with more experience should refine my proposal if either of this is ever going to be (even) considered to be implemented. I am obviously just throwing some ideas.

3 Likes

My problem is not mapping the success or failure types. By using mapError I just get a different kind of error type, but unwrapping it is still exactly the same as before. I actually don't have any problems with mapping the types, that's not what I'm trying to solve.

Also, I don't want to do all my work inside of flatMap closures. I find them hard to read and reason about. I prefer structured programming where I can read the code top-to-bottom as much as possible. This also makes it simple to have multiple failable calls in a row without deep nesting.

3 Likes

You are very precisely describing the challenge. I have had this though many times, although not this precisely and concisely put. I have tried to describe the problem on "Using Swift" but had no useful responses.

I really like the strongly typed and explicit nature of Result, just like you describe.

Your solution is really good in my opinion. I think the syntax you propose has really good ergonomics and improves code readability a lot.

I do not particular like the use of catch as keyword but I have a hard time suggesting anything better.

I do not like the auto-generated let error in the normal do-catch and I do not like it here. I would prefer to have the explicit form catch let error be the only one allowed.

Those are very minor objections and I would love to see this added to the language. It would improve the ergonomics of using Result a lot.

1 Like

I feel the exact same way! I agree 100 % with both you original post and the answers you have given to objections and very much with this description. You are putting your energi a place where I had given up because of the responses I have seen so many times her on the forums.

I really hope you keep up the effort. It is much appreciated! Very much.

2 Likes

I really like your explanation here. It clearly shows that there definitely are important scenarios where having a feature like you pitched here would be really nice.
However, as others pointed out already, your pitch seems to actually just be a part of the much larger feature typed throws that was already heavily discussed a while ago. Even the core team (or at least members thereof) showed some interest at some point. However, the discussion somehow has come to nothing and slowly died off. Maybe you could revive it by providing your excellent explanation there. Because your use-case would definitely be covered by the larger feature and this solution would fit the language much better, IMHO, since it doesn't introduce a little feature that goes against anything related in Swift, but rather really fixes the underlying error.

EDIT:
Just scrolled to the bottom of that thread and saw that you already engaged in the discussion there with a similar argument. Nevermind then

1 Like

I would be supportive of being able to have syntactic sugar to treat Result as Optional ( failure case with an empty error)

let myResult: Result<String, Error> = .success("Hello")

// sugar: guard let someResult = myResult else {
guard let someResult = try? myResult.get() else {
	 fatalError()
}

print(someResult + " World")

Then we can have if-catch-else, guard-catch-else where catch would only trigger if there is an error. I am just not sure how successful a proposal in this area is going to be. I use go nowadays and Swift’s error handling is so much better.

I'd be in favor of allowing this:

let someResult: SomeType
do {
    someResult = try result.get()
} catch {
    // handle error
    return
}

to be shortened to something like this:

do let someResult = try result.get() catch {
    // handle error
    return
}

That sounds a useful way to handle errors. Note that do-let here does not unwrap an optional, hence why it's not if-let or guard-let.

I also think typed throw would be great.

If you combine the two, you get pretty much what is requested here in a way that I think makes a lot of sense. But obviously the implementation cost (especially of typed throw) is much higher than a simple sugar over Result.

7 Likes

Or this:

let someResult = do try result.get() catch return

as a "syntax optimised" version with brackets:

let someResult = do try { result.get() } catch { return }

can be used unrelated to Result.

[UPDATE]
I just added this collapsible paragraph to the initial post as I had a random thought that further confirms my notion of comparing Result to Optional and asking for similar syntactic sugar:

About the relationship between `Result` and `Optional` While not exactly true, in a certain way an `Optional<T>` can be seen as nothing else than a `Result<T, EmptyError>` where `EmptyError` is a theoretical type that contains no specific information about why the `Result` has no value, turning the `failure` case into a mere statement *that* the `Result` doesn't have a value – much like `Optional.none`. From that perspective, `Result` is simply an abstraction over `Optional` and `Optional` the specific case of a `Result` where the reason for not having a value is stripped/empty. Therefore, these types can be considered to be closely related.

There's a term that abstracts this similarity – it's monad.
Result, Optional, Set, Array are all that.

Optional can really be considered to be an array of type T with maximum count of 1.
Result cannot – you can fit it into a single-type wrapper.

Nevertheless, Result, on the other hand, is also a sum type that wraps two separate types, and that is what is leveraged in whatever we do with Results. While Optional could also be considered that, (an enum of .some and .none), this is not the property that was used to embed Optional into the language.

I'm sure there's some mathematical name that would formalise this difference that separates an implementation that wraps a single type, and one that wraps two types.

You can flatmap over Optional, but for Result, you have two flatmap operations. That's key.

Sounds very theoretical to me. I’m very practical.
And from my point of view, the semantical meaning of Optional and Result are very close. Both represent some kind of success or error, but one with extra info on the error and the other without. To me, the semantics is more important than the structure.

2 Likes

I agree, but I’m responding to that box and my rambling way of expressing myself doesn’t make things easier. Structure imposes limits to what you can do and how you can express it. Syntax is purely a structure.
I want to say that even though we might use them similarly, their structural difference means that they aren’t analogous syntactically. And when we’re speaking about syntactic expressions, these limits aren’t unimportant.
And that difference is the presence of two types in Result, thus requires two elements to pattern match, while only one suffices for Optional. We can express Optional as Result, but we cannot express Result as Optional. Optional is “is A / isn’t A”, Result is “is A / is B”. So similar syntactic sugar might not be possible without reconsidering the whole if-guard-let figure from the ground up to make Result as its main model rather than Optional (and Boolean from which it inherits).

I might be wrong about its importance. I want Result to be upgraded like this.

1 Like