Second draft: Sum Type by Struct
Richard Andrew Cattermole (Rikki)
richard at cattermole.co.nz
Fri Sep 4 08:20:42 UTC 2026
On 04/09/2026 7:30 PM, Meta wrote:
> On Thursday, 3 September 2026 at 11:45:12 UTC, Richard (Rikki)
> Andrew Cattermole wrote:
>> ...
>
> My main critiques:
>
> 1. This is a structural approach to discriminated unions that
> is a poor fit for D's (mostly) nominal type system.
> Specifically, I think the implicit widening is a bad idea
> because it could lead to confusion when mixing sumtypes from
> completely different domains which just happen to be implicitly
> convertible via said widening.
>
> 2. The justification of being able to chain matches and
> interleave them with range code is not compelling enough to
> make the unfamiliar match syntax worthwhile in my mind (some
> examples of how this will improve existing code or new patterns
> it would enable would help).
UFCS is touted as one of D's major strengths.
It gets mentioned repeatedly.
Right along with input ranges.
Match expressions are inherently unary, and ``Expression .
Identifier OpenPunctuation`` is an already existing pattern in
our grammar.
I suspect those who don't like it, will find it will grow on them
over time and wonder why everyone else did it a different way.
> 3. Also, it makes built in language syntax look like
> user-written delegates, which will be confusing.
That's intentional.
It supports both by-ref and multi-level matching.
If we need to expand it we would have the capacity to do so,
although at this stage I don't have a reason to.
> 4. Match arms being allowed to return separate types and
> silently evaluating to a newly synthesized sumtype is just
> asking for trouble. It's going to be a source of a lot of
> frustration and compiler errors, and potentially even bugs.
> What other language that has sum types does this?
If you don't have widening support you get an error.
If you expect it to not widen but does, you get an error.
If you wanted it to widen and does, it works.
It does do integer promotion, so cases like that won't widen, if
there are other cases I haven't considered like const or char
then we can add that as a bug fix.
TypeScript:
```typescript
function evaluateArm(input: "a" | "b"): string | number {
switch (input) {
case "a":
return 42; // This arm evaluates to a number (int)
case "b":
return "hello"; // This arm evaluates to a string
}
}
const result: string | number = evaluateArm("a");
console.log(result);
```
Scala
```scala
def evaluateArm(input: String): Int | String =
input match
case "a" => 42 // This arm evaluates to an Int
case "b" => "hello" // This arm evaluates to a String
case _ => 0
@main def run(): Unit =
val result: Int | String = evaluateArm("a")
println(result)
```
Ocaml
```ocaml
type input_type = A | B
let evaluate_arm (input : input_type) =
match input with
| A -> `Int 42 (* This arm evaluates to a polymorphic
variant containing an int *)
| B -> `String "hello" (* This arm evaluates to a polymorphic
variant containing a string *)
let () =
let res = evaluate_arm A in
match res with
| `Int n -> Printf.printf "Int: %d\n" n
| `String s -> Printf.printf "String: %s\n" s
```
> 5. "No static foreach and so on support for the match handlers."
>
> For me, this is a hard requirement for any such proposal. Maybe
> others will disagree.
Previous design supported it.
It would be a huge amount of work, for something that could be
solved with:
```d
(v) => () {
...
}()
```
If it is a problem, then maybe a follow up DIP can do it, but I
don't think the ROI is present for me to do it atm.
> 6. "The library form remains fully supported and continues to
> serve code that needs template-computed variant sets or cannot
> migrate. The language form is additive."
>
> I think the minimum baseline for any successful proposal is
> that it can completely replace std.sumtype. Otherwise, what's
> the point? FYI the library solution becomes completely
> redundant with my proposal:
The point of that statement is that it won't go round breaking
code.
This DIP exceeds what std.sumtype can do.
> ```d
> enum union SumType(Variants...)
> {
> alias Types = NoDuplicates!Variants;
>
> static foreach (i, V; Variants)
> static if (isBuiltInType!T)
> case V;
> else
> mixin($"case $(T.stringof) = T;"); // type
> aliasing/punning e.g. case ExternalStruct = ExternalStruct
>
> // Implement the other member functions from std.sumtype
> }
> ```
>
> And Variant/Algebraic/Nullable for that matter.
Unfortunately it doesn't solve for result types in general, it
can't.
Result types require two language integrations, opCast, and some
way to check then unwrap like opUnwrapIfTrue.
So neither proposal solves for Nullable which is a result type.
Also Variant isn't a tagged union, so it uses TypeInfo, so not
replaced either.
> 7. "Catch-all arm: (x) => 42 — matches any variant, parameter
> type inferred from variant"
>
> How does this work? In std.sumtype it works because the
> template lambda acts as a catch all whose type is inferred, but
> I don't think that can work outside of a template function
> context (?) Is the type of x a synthesized union containing the
> types that weren't matched?
The body gets copied for each arm that's needed, so it has a
unique variant per instance.
Which is the same mechanism with a lambda which hasn't got all
the parameters fully specified. But unlike the lambda, this
doesn't use templates to do it.
> 8. "If any unnamed variant has a type whose identifier is None
> (e.g., struct None {}), that variant becomes the default. The
> tag is initialized to that variant's index, and the
> corresponding variant field is initialized to its type's .init."
>
> I don't like this special casing. Why not at least use
> typeof(null) instead, which is already D's built in unit type?
> (And void, but unlike typeof(null), you can't create a value of
> type void). It should be that the .init value of the union is
> the init value of its first syntactically declared member. So
> you just put your None case first, and that becomes the natural
> default.
I would prefer to use identifier types instead, they don't have a
declaration associated with it. However I don't see a way to get
them approved right now. Previous designs used them.
It was part of my member of operator work, but due to their
implicit conversions it was just going to be an uphill battle
with Walter either not understanding it or not liking it.
> I think the rest of it looks good. I'm not crazy about the
> sumtype syntax, but it's not a deal breaker (though I think the
> syntax in my proposal is much better 😉).
More information about the dip.development
mailing list