First Draft: Nominal Sum Types via `enum union` and `switch` Expressions
Meta
jared771 at gmail.com
Wed Sep 23 22:23:46 UTC 2026
On Wednesday, 23 September 2026 at 22:15:09 UTC, Meta wrote:
> 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);
> }
> }
> }
> ```
That can be simplified to:
```d
enum union DynamicNumber
{
case long, float, double, error(string);
}
DynamicNumber sum(DynamicNumber a, DynamicNumber b) @safe
{
enum isBare(alias V) = __traits(variantKind, V) == "bare";
alias NumericVariants = Filter!(isBare, __traits(allVariants,
DynamicNumber));
return switch (a)
{
case error => a,
static foreach (V1; NumericVariants)
case V1 v1 => switch (b)
{
case error => b,
static foreach (V2; NumericVariants)
case V2 v2 => DynamicNumber(v1 + v2),
},
};
}
```
Actually that's pretty straightforward for a fairly complex case.
The only thing that's needed is static foreach support for
generating switch arms, which is on my todo list already.
More information about the dip.development
mailing list