Precise error typing in Swift

Slightly off-topic, I don't want to derail the thread from it's main topic.

One way some Swift community expressed a possibly solution for this problem is an enum with variadic generic type parameters, that itself would possess indexed cases just like tuples or even labels iff generic type parameter will support this in the future.

I'm going to use A | B syntax as a shorthand for such special enum.

let value_1: A | B = ...

switch value_1 {
case .0(let a): // A
  ...
case .1(let b): // B
  ...
}

let value_2: A | A = ... // still valid!

// maybe in the future
let value_3: (x: A | y: B) = ...

switch value_3 {
case .x(let a): // A
  ...
case .y(let b): // B
  ...
}

Such an enum is NOT a OneOf type, but fairly close and possibly still within the Swift's standards.

3 Likes

Could you elaborate on this constraint? If we want to encourage users to switch most uses of rethrows over to the "trivial" signature which just forwards the generic error parameter, why is it a requirement to support the expression of proper rethrows semantics with the typed throws feature? Would it not suffice to say "we expect future code to just forward the errors directly, but if the precise semantics of rethrows are desired, it will continue to be supported (though no future evolution should be expected)"?

The main thing you can express in rethrows that you otherwise can't is something that conditionally throws but changes the error type. I think there are two reasons we might want to support that:

  • It's still useful to be able to write that kind of combinator with precise error types.
  • It's nice to have a single set of rules built around common concepts.

I agree that it could probably be subsetted and the complexity left as an internal implementation detail.

2 Likes

Instead of introducing errorUnion which is specific for combining error types,
I personally would like to see more general (Open) Union Types to be introduced in Swift first,
which adds more value in error handling.
(NOTE: Union types natively exist in TypeScript, and recently Scala 3)

For example:

// Swift's (Closed) Enum
enum MyError: Error { case process1Error, process2Error }

// BAD: Using poor error domain which already mixes 1 & 2.
func process1() throws(MyError) { ... }
func process2() throws(MyError) { ... }
func process1And2() throws(MyError) { ... }

can be improved using Union Types:

struct Process1Error: Error {}
struct Process2Error: Error {}

// GOOD: Error domains are nicely separated per func and then combined.
func process1() throws(Process1Error) { ... }
func process2() throws(Process2Error) { ... }
func process1And2() throws(Process2Error | Process2Error) { ... }

so that error union will be expressed seamlessly as Process1Error | Process2Error
rather than errorUnion(Process1Error, Process2Error).

In this case:

will need a change to:

errorUnion(T, U) == T | U

which I think is more precise error typing for Swift.

1 Like

There is of course a middle ground, that is, anonymous enums, that would be for enums what tuples are right now for structs.

I'll not go down this path again, I tried to show their utility in the past but given that they would represent the dual structure to tuples, they are pretty clearly useful for the dual reasons. Being able to sum multiple error types without the need to create a specific enum only for that is just another example. The most compelling arguments I read against this are mostly related to clarity of syntax, but for injection/projection we could actually use ._0, ._1, which would match the synthesized Codable keys recently added to Swift. Anonymous enums would follow the same protocol conformance rules we're adding for tuples, that is, Equatable, Hashable, Comparable, and we would need to add Error if all cases are Error.

Multiple Either# types added to the standard library would also work, but would be worse in terms of flexibility (no option to declare specific tags for cases), and would essentially represent a load of boilerplate.

2 Likes

We're not going to discuss anonymous enums or union types in this thread. If there are more posts about them, I will just move all of those posts into a new thread.

9 Likes

May I offer a syntax suggestion to address the tension between needing typed throws, but not wanting typed throws everywhere?

If typed throws where spelled differently, they would stand out. They would be easy to find and build linter rules for. Documentation for typed throws could mention their use cases along with a warning for the anti patterns they have.

I'm not sure if this is the time or place for this suggestion, forgive me if it isn't.

Adding keywords to solve a design problem is something to be careful with; otherwise the language becomes bloated. For low level tools adding keywords might be warranted.

A typed throws could be named throwsWithType and similar for rethrows etc.

I'd like to put into the table the proposal made some time ago that was rejected over the discussion mainly because lack of people with resources and enough knowledge about the complier to bring this into a formal proposal and the negative from done members of adding this, but being, as an author of it, as much concise and detailed as I was capable of with the help of many members to bring this thread into this discussion.

Great to see this topic being brought into discussion.

Terms of Service

Privacy Policy

Cookie Policy