Second draft: Sum Type by Struct
Meta
jared771 at gmail.com
Sat Sep 5 06:19:04 UTC 2026
On Friday, 4 September 2026 at 08:20:42 UTC, Richard (Rikki)
Andrew Cattermole wrote:
> 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.
That's a weird conflation of two completely unrelated things.
> 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.
I don't care that much, but I'm not the person who's just
learning D. They might take issue with the language having
exactly one block syntax that's completely different from every
other one in the language, in a way that tries to disguise it as
user code.
>> 3. Also, it makes built in language syntax look like
>> user-written delegates, which will be confusing.
>
> That's intentional.
Ya I got that, and for the life of me, I can't figure out why.
> 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.
We need full blown pattern matching, or what's the use of
introducing a match statement? There's no point to half measures
here:
```d
Result!(int[]) result = Result!(int[])([99]);
auto first = switch (result) {
case Some([]) | null => -1,
case Some([head, rest...]) => head,
};
```
>> 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);
> ```
TypeScript has a very ML-like, structural type system. It can
also synthesize new types on the fly at runtime which D can't do.
> 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)
> ```
Scala is the only language that really lends to your point here.
Nobody uses Ocaml for programming outside academic and hobby
contexts.
> 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) => () {
> ...
> }()
> ```
Ah, I thought you meant that it's not possible to generate match
arms with static if.
> 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.
1. D already has opCast
2. That's an arbitrary requirement you made up that has nothing
to do with Result types.
That being said, the if-let pattern in both Rust and Swift
accomplishes this in a much simpler way:
```rust
Option<int> result = Some(99);
if let Some(n) = result {
...
} else {
...
}
```
> So neither proposal solves for Nullable which is a result type.
Both definitely do.
> Also Variant isn't a tagged union, so it uses TypeInfo, so not
> replaced either.
That's an implementation detail, but my proposal could also
easily use a runtime type info based approach, and be much
simpler.
>> 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.
I'm pretty sure this is the first time in D something like this
would work outside a template/static if context.
> 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.
It basically does the same thing as templates do.
>> 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.
Why do you need that? Just tell the programmer to make the first
type of the union its None value, and adopt the rule that the
union's init value is that first type's init value. It's such a
simple solution rather than having the compiler recognize a
special, arbitrary symbol.
>> 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