Personally, instead of adding a new kind of type, I would just introduce the concept of a closed protocol. It's not exactly the same as a structural union type, but it's so similar to normal (open) protocols that it barely add any new complexity to the language while covering pretty well what you'd do with a union type.
closed protocol Acceptable {}
extension Int: Acceptable {}
extension String: Acceptable {}
// conformances are only allowed in the same module so the compiler
// always knows the full list of conforming types
In usage it works exactly like any other protocol, with only a bit of convenience added, like we can exhaustively switch
over the the existential box:
func accept(_ value: any Acceptable) {
switch value {
case i as Int: print("accepting Int \(i)")
case s as String: print("accepting String \(s)")
}
}
(We might need an @unknown default
in other modules, like for enums, for case new cases that could be added later.)
Usage:
accept(1)
accept("hello")
var a = 1 as any Acceptable
a = 2
a = "hello"
The main difference would the that this protocol's existential box is more efficient as its underlying implementation could work like an enum. And method dispatching could also be done with a switch
instead of witness tables. I suppose this would be beneficial for embedded Swift.
And if we define another protocol encompassing all the same types (opened or closed), casting with as
is allowed. For instance:
protocol Rejectable {}
extension Int: Rejectable {}
extension String: Rejectable {}
let r = a as any Rejectable
// casting allowed without as? or as!
// because all Acceptable types are also Rejectable
Here we can use as
to convert from one type to another because all the types in the closed protocol are known to be compatible with the requested type. This is not an implicit conversion though: just an explicit cast that can't fail. We continue to use as?
if there's a chance the value is not part of the destination type:
let i = a as? Int
let b = a as? any BinaryInteger
// casting allowed but may fail
Whether this approach is beneficial for typed throws is another question though. If you want to automatically combine all the types thrown in a do
block to form a union type, you need to be able to merge types to form a new union type. This is a bit muddy with protocols.
With closed protocols you'd have to write things like this:
do throws(any AcceptableError) {
try accept()
try acceptAgain()
} catch {
// ...
}
This already works with a normal (open) protocol, so it'd be nothing new... except now you can catch exhaustively all the types in the closed protocol (like with the switch
above). The price is you have to choose the protocol beforehand in do do throws(...)
.