[Re-Proposal] Type only Unions

For me, the main benefit of Type Unions would be dealing with Type errors.

I know this has been rejected, but now that we have full typed errors within Swift, we may need to start to consider Type Unions again.

For example, currently, if you’re working with async code, you will likely want to handle CancellationError as thrown by Task.sleep and Task.checkCancellation(), so if you adopt any typed throws, you need to be able to deal with a union of CancellationError | MyCustomError.

To be explicit about this:

Sure, you could wrap any call to a Task method that may throw and capture the CancellationError then places it within a custom MyUnionErrorContainer with a case for your errors, etc.; however, this falls apart as soon as you need to use other Task-related operations like withThrowingTaskGroup.

However, if you were to wrap the CancillationError in a custom enum, then the standard lib would have no way to extract the CancillationError from this and would rethrow your wrapped error (cancelling all other tasks).

Exception handling, it is extremely common for a third-party library (or system lib) to handle some exceptions but re-throw others, so this is a very common pattern.

If we want typed errors to be adopted, and we want the standard library to be able to expose them (eg have a ThrowingTaskGroup that throws a typed error from its content), then we need to be able to have a Union of types on these errors. The same is true for many other standard library methods that currently do not support typed errors, as while most of the time they just re-throw the wrapped closure, they may sometimes throw their own type or swallow some exception thrown from the wrapped function.

Key to this woudl be to ensure the Type Union system is able to support generic types as well;

For cases were a function may throw an additional error to those in the closure it is handling

func decodeComponents<E: Error>(_ block: (String) throws(E) -> Int) -> throws(E | DecodingError) -> [Int] 

and for cases were a method swallow an error:

func search<E: Error>(_ block: (String) throws(E | CancelSearchError) -> Bool) -> throws(E) ->  Int?
1 Like

I understand why we are discouraged from using typed throws. The proposal very clearly stated how harmful they can be. But they simply exist and there are certainly cases where "catch them all" is viable.

For starters, a custom enum Union2<T, U>: Error where T: Error, U: Error (and higher arities) will do.

But in generic contexts like

the E can already be Union2 which would result in propagating nested unions. And this is why I find a very lightweight union feature somewhat compelling. Simply because statically summarized ErrorA | ErrorB is swiftier than reducing some Union2<ErrorA, Union2<Union3<Never, ErrorB, ErrorA>, Never>>.

There certainly are cases where expecting a couple of errors within a module is neither a cardinal sin nor something that should make you feel guilty of being a Java developer. However, propagating in generic contexts takes a toll without unions.

I've noticed the mention of "closed" protocols (also discussed on the forum as "sealed"). While I don't think they would solve the problem of typed throws exhaustion, perhaps the unions could help to declare a closed/sealed protocol?
public protocol Beverage where Self ~= Water | Espresso | Beer
Take it with a grain of salt; I'm anything but a compiler engineer.

I remember times when typed throws were a heresy, and here we are. It just needs time until the core team finds a need for it; just like Task brought us the typed throws.

I don't think this is always viable, if you are for example providing a closure to a third party library (such as a dynamically linked standard library function, system api etc) that has some behaviors based on errors you throw this third party lib is not going to be able to deal with a custom enum you create within your closure.

Over the years of using Swift, I have changed my view on this subject from negative to neutral.

Just like tuples can be used as lightweight, anonymous structs, If some day there's a syntax for Swift to describe anonymous enums, I will surely welcome it with my arms.

1 Like