The reason that Swift proposals make use of motivating examples is not merely because "everyone loves examples." Swift is a pragmatic language; this means that its features are meant to help users solve actual, real-world problems, and its design is meant to encourage users choosing the best solution by default. Any theoretically sound idea must nonetheless accomplish these practical goals in order to be fit for inclusion into Swift.
With this in mind, let's evaluate what you've laid out here in terms of examples and what they reveal about any possible design for anonymous sum types, starting from the bottom:
What you demonstrate here is that an ergonomic implementation of this feature would require sophisticated subtyping relationships.
Here, you are stating that (Action | Change | SideEffect)
must be considered a subtype of (Action | Change)
; by induction, too, you'd need—and want—Action
to be a subtype of (Action | Change)
.
To complete the feature, you would want sort of equivalence between (Action | Change)
and (Change | Action)
. This would be so both for theoretical (and user expectation) reasons, since semantically the operation is commutative, and for practical reasons, since you don't want users to have to go through a dance every time one API returns (A | B)
and another takes an argument of type (B | A)
.
We have an ad-hoc tuple shuffle feature that allows a value of type (a: Int, b: String)
to be assigned to (b: String, a: Int)
. @codafi has been trying to get rid of it for a very long time; as he writes, just that feature alone "complicates every part of the compiler stack."
Put simply, there is no way to create what you call "a more powerful type system, without renouncing simplicity and accessibility." It is why the core team has said of this feature:
Disjunctions (logical ORs) in type constraints: These include anonymous union-like types (e.g.
(Int | String)
for a type that can be inhabited by either an integer or a string). "[This type of constraint is] something that the type system cannot and should not support."
Fundamentally, there is nothing revealed here that would surmount this barrier; what you imagine here is not possible to implement as part of Swift's type system.
Could there be advantages for the author of the API in allowing another way of organizing code? Sure. But we must consider the user of the API, and here's where things fall apart.
Now that we've established that the type system relationships outlined above are impossible, that leaves explicit type conversions at every point of use. This is wildly unergonomic for the user of the API, not to mention visually noisy. Consider:
// Using overloads:
event.handle(action)
event.handle(change)
// Using anonymous sum types:
event.handle(.action(action))
event.handle(.change(change))
Moreover, for equivalent performance, the compiler would have to optimize the code so that wrapping a value in a sum type and then unwrapping it are no-ops. Currently, they are not no-ops.
This is a good example of how a feature might cause a negative effect on other aspects of the language. In this case, adding what seems like an equally attractive (or perhaps more attractive) option for authors of the API that ends up being worse for the end user would be opposite to Swift's goal of encouraging the best solution by making it the easiest solution.
Consider what would happen if you'd try to model this type and there were milk, cookies, and bread. The absurd result that you'd obtain clearly demonstrates that this is not how we want to model a 'non-exclusive or.'
If we wanted to model these type relationships for the uses you'd describe, then it'd be perhaps appropriate to consider Milk ^ Cookies
as the more accurate notation where you need one or the other, while Milk | Cookies
would allow one or both.
Again, though, you're describing a type system that cannot be implemented in Swift.
We tried to do this once with an explicit type, ArithmeticOverflow
. We had to remove this type in a revision to the proposal after the feature shipped. The reason was that, although self-documenting, the user experience was extremely silly.
This was because there is nothing you can do to generate such a result except to take a Bool
and then convert it—and even to do that in one line, you'd want to create an initializer for your type, which you can't do for an anonymous type. There is also nothing that you can do with such a result except to convert it to a Bool
. So what you're left with is instantiating something you immediately then discard—and, as mentioned above, this isn't optimized away, so you are paying a performance penalty for it too.
It's a good example, actually, of how Swift aims to be pragmatic, and what that means. On paper, it seemed like a decent idea to create ArithmeticOverflow
for reasons similar to the ones you've outlined here; in practice, it failed to improve the user experience.
OK, I've written enough, and there are other things requiring my attention. I hope that this post has given some pointers both about the high-level design philosophy of the language as well as some clarifications of what is and isn't possible to accomplish with this feature in particular.