First Draft: Nominal Sum Types via `enum union` and `switch` Expressions
Meta
jared771 at gmail.com
Wed Sep 23 22:15:09 UTC 2026
On Wednesday, 23 September 2026 at 09:14:43 UTC, Dukc wrote:
> On Monday, 21 September 2026 at 14:17:51 UTC, Meta wrote:
>>> This means that `int*` is more specialised than `void*` and
>>> should therefore be picked.
>>
>> Ya, I meant for that to be long*, not void*.
>
> Now sounds right to me.
>
>>> Well you are already specifying your sum types are structs in
>>> disguise, which is likewise out of scope for what is
>>> minimally required for sum types.
>>
>> How else would they be represented? A struct that contains a
>> union has been the canonical way to do this for the past 50
>> years, and it's a very easy, simple lowering.
>
> Some inprecision from my part again. I don't actually care much
> how the compiler will lower the tagged union, that's an
> implementation detail only unless you spec it. I mean your
> specification that additional (non-`case`) non-`static` fields
> will behave as in `struct`s (or `class`es).
>
> It's not unreasonable, but it's not the only choice. The
> additional fields could alternatively behave as in a regular
> `union`s like I proposed, or the compiler might simply reject
> them. The latter choice would be the minimalist one.
Ah I see. They're useful for having members that are valid for
every variant. Initially I had a "common field promotion" rule,
where fields that are common to every variant can be accessed
directly instead of needing to use a switch expression, but I
think allowing the enum union itself to have struct-style fields
is a better design.
>>> I guess changing the keyword would be an okay alternative for
>>> me.
>>
>> `enum union` is just a placeholder name, but it also makes the
>> most sense to me. They have aspects of both enums, with the
>> enumerated cases, and unions, with the overlapping storage.
>> Simple.
>
> Except the storage stops overlapping if you use non-`case`
> fields.
Yes.
>>>
>>> The question is, what are_those invariants in this case? You
>>> can specify that a sum type must have a valid tag on
>>> destruction or else undefined or unspecified behaviour will
>>> follow. But I don't think that's a good idea, since it isn't
>>> possible to skip the destructor call of a local variable
>>
>> Named Return Value Optimization.
>>
>
> If you're returning the sumtyped value, that is. Not always the
> case.
>
>>> (except by things like `longjmp` or foreign language
>>> exceptions). Hence my suggestion to specify the destructor
>>> will do nothing if the tag is invalid.
>>
>> You're wildly over-complicating this. If the user wants to
>> mess around and do @system stuff with an enum union, they're
>> on their own.
>
> I'm not sure my suggestion is actually complicating anything.
> It is literally the simplest behaviour possible to do nothing
> on invalid tag. My suggestion would basically just ban the
> compiler from executing a random destructor on an invalid tag
> or the optimiser assuming the tag will always be valid.
The tag can never be invalid. That's a core assumption of the
feature. If the user does some hacky stuff that messes with the
tag, they're responsible for making sure it's valid again
afterward.
>>> I basically mean that if you have `switch (sum) { case x(y)
>>> => smth, ...}`, it could either mean that `x` is a variant
>>> and `y` is a variable declaration for `smth`, or that `x` is
>>> a function or a constructor called with value `y`, and `sum`
>>> is matched against the result (as in existing switch
>>> statements).
>>
>> It's invalid to have a function call here, so there's no
>> ambiguity.
>
> Doesn't that feel limiting? Consider matching a primitive type.
> Do you feel this should not be allowed?
>
> ```d
> import std.conv;
>
> auto var = case(aString.front)
> {
> case '\r' => fun(),
> case '\n' => gun(),
> // Unicode replacement character
> case to!dchar(0xFFFD) => hun(),
> };
> ```
No, it shouldn't be allowed.
> Remember, existing `switch` statements allow this. Why `switch`
> expressions wouldn't?
Because switch statements match against values, while switch
expressions match against patterns. Patterns only exist at
compile time, so a function call cannot be a pattern.
>>
>> I'm fine with that. If you think it's worthwhile, you will
>> need to show me a very convincing example of why it's useful.
>
> ```d
> enum union DynamicNumber
> { case long, case float, case double, case error(string);
> }
>
> @safe sum(DynamicNumber a, DunamicNumber b) => switch(a)
> { case error(s) => a,
> case numA => switch(b)
> { case error(s) => b,
> case numB => numA + numB,
> },
> };
> ```
>
> This is a lot more verbose to do manually with `static
> foreach`s, especially if you're going to stick with colon
> separators for the match cases as opposed to semicolon
> separators so that you can't place the `static foreach` inside
> the switch statement.
The issue is that the only place in the language where this
exists is in foreach/static foreach, and in template lambdas. In
the foreach/static foreach case, the body is copied N times for N
different types. That makes some sense for a loop, but IMO it
makes *no* sense for a switch case. Template lambdas infer the
argument type, which can't be done in this case because which
variant is active is only known at runtime.
Also, since you're omitting the types here, they would have to be
inferred by the compiler. I *think* it can infer them, and the
inferred type would have to be double, but that would be very
confusing for the reader. There are a lot of issues with this
suggestion.
That being said, you're right that this would be tedious to do
with the proposal as it is now.
For this specific DynamicNumber case, this *should* work (should
as in it doesn't right now because static foreach isn't allowed
in a switch expression, but should be made to work):
```d
enum union DynamicNumber
{
case long, float, double, error(string);
}
DynamicNumber sum(DynamicNumber a, DynamicNumber b) @safe
{
enum errorTag = __traits(variantTag, DynamicNumber, "error");
if (a.__tag == errorTag)
return a;
if (b.__tag == errorTag)
return b;
enum isBare(alias V) = __traits(variantKind, V) == "bare";
alias NumericVariants = Filter!(isBare, __traits(allVariants,
DynamicNumber));
return switch (a)
{
// Already handled the error case above, but this is
needed to satisfy the exhaustiveness check
default => assert(0),
static foreach (V1; NumericVariants)
static foreach (V2; NumericVariants)
case V1 v1 => switch (other)
{
case V2 v2 => DynamicNumber(v1 + v2);
}
}
}
```
More information about the dip.development
mailing list