First Draft: Nominal Sum Types via `enum union` and `switch` Expressions
Nick Treleaven
nick at geany.org
Mon Sep 14 16:04:35 UTC 2026
On Monday, 14 September 2026 at 06:35:13 UTC, Meta wrote:
> I was inspired by Rikki's DIP
> (https://forum.dlang.org/post/nhbiwarfrlqqffegkhsf@forum.dlang.org) to write my own that introduces sum types from a different direction that I feel is more "D-like". It borrows heavily from Rust and Swift's version of the feature.
Somewhat like Rikki's DIP, I think this is too complex for a
first DIP. It would be better to focus on the core functionality
and extend it later. Some of this could maybe be marked as plans
for later enhancements.
> When it is ambiguous which type would be initialized by this
> assignment, the compiler requires the user to disambiguate:
> ```d
> enum union Nums
> {
> case int,
> case long,
> }
>
> Nums n = 0; // Error, 0 is ambiguous between variants `int` and
> `long` of enum union `Nums`
> Nums n = 0L; // Ok
> ```
Why is 0 ambiguous? 0 has type `int`.
> ## Enum Union Members
>
> Enum unions are treated as struct declarations internally,
> which contain a union with the declared variant cases, and a
> `__tag` value to track which variant is currently active.
>
> Like other aggregates in D, enum unions can contain members,
> member functions, constructors, destructors, aliases, etc.
> ```d
> enum union NetworkMessage
> {
> case Heartbeat(),
> case Text(string content, string encoding),
> case Binary(ubyte[]),
> case Status(int statusCode, string statusText); //
> Terminating semicolon delimits variants
Why not require semi-colon after every case then, instead of
comma? That would be simpler, and consistent with field
declarations in a union/struct/class.
> ## Implicit Construction
>
> Enum unions are implicitly constructed in the following cases:
> the struct-style construction via assignment discussed
> previously, when a function takes an enum union as an argument,
> and when a function returns an enum union:
> ```d
> enum union Option(T)
> {
> case None = typeof(null),
> case Some(T),
> }
>
> Option!ConfigValue getConfigVal(string name) {
> string[string] config = readConfig("config.csv");
> if (auto val = name in config) with (typeof(return)) {
> return Some(ConfigValue(*val)); // Implicitly
> constructs an Option!ConfigValue
> }
Above, how is that implicit construction? It just uses `with`.
> ```d
> return null;
> }
>
> void applyConfigVal(Option!ConfigValue c);
> applyConfigVal(null); // Implicitly constructs
> Option!ConfigValue.None
> ```
I like that, and it is a reason to have sum types be built-in to
the language. Rikki gave a list of reasons, but most of them seem
to be applicable to a library sum type at least in theory.
> ## Niche Optimization (not yet implemented)
>
> When an enum union contains unit variants alongside
> non-nullable references, pointers (`T*`), class references, or
> bounded scalars (such as `bool`), the compiler exploits invalid
> bit patterns to encode the unit state:
>
> * `Option!(int*)`: The null pointer address `0x0` represents
> `None`.
> * `sizeof(Option!(int*)) == 8` (on 64-bit platforms), incurring
> zero byte overhead for the tag.
Surely that only works with one unit variant and one pointer?
> There may only be **one** pattern per variant. The following
> will not compile:
> ```d
> switch (pkt)
> {
> case Data(bytes) => ...,
> case Data(bytes2) => ..., // Error: redundant match arm.
> Pattern is unreachable
> }
> ```
>
> Every arm must start with the `case` keyword, and every arm is
> required to produce a value.
Why require a value? Actually I've just seen the example under
'Side-Effects' - apparently it doesn't require a value.
> Arms may not contain statements; only a single expression that
> produces the value for that arm. Thus, the following will not
> compile:
> ```d
> case Data(bytes) => {
> writeln("Received Data payload");
> ...
> return format(...);
> }
> ```
>
> However, statement blocks can be emulated using an
> immediately-called delegate literal:
> ```d
> case Data(bytes) => {
> writeln(...);
> ...etc.
> return format(...);
> }(),
> ```
That is not equivalent - in the first block `return` returns from
the function containing the `switch`, in the latter, the `return`
gives the result of the case arm.
I showed how the compiler could lower a declaration initialized
from a match/switch construct to a series of statements here:
https://forum.dlang.org/post/gljmaiuvrjimeduohhuq@forum.dlang.org
The same principle applies for a match/switch statement with no
result.
> ### Destructuring Patterns
> As shown above, destructuring patterns destructure the enum
> union's variants. Destructuring patterns can be used for unit,
> tuple, and structure variants.
>
> Destructuring patterns allow fields to be omitted using `...`
> syntax:
How often is it needed? This could be an enhancement DIP, as it
could apply to tuple declaration unpacking as well. Also `_`
syntax to ignore a single item could be a part of that DIP too.
> The `...` syntax allows ALL fields to be omitted:
> ```d
> case Unit(...) => 1, // This is valid because ... means "0
> or more fields"
> case Struct(...) => "No access to Struct's fields here",
> ```
Hold on, one case arm returns int and one string. What is the
result type of the switch expression? I assumed it was the common
type of all case arm result types.
More information about the dip.development
mailing list