A proposal: Sumtypes

Timon Gehr timon.gehr at gmx.ch
Thu Feb 8 19:26:37 UTC 2024


On 2/8/24 16:42, Richard (Rikki) Andrew Cattermole wrote:
> Yesterday I mentioned that I wasn't very happy with Walter's design of 
> sum types, at least as per his write-up in his DIP repository.
> I have finally after two years written up an alternative to it, that 
> should cover everything you would expect from such a language feature.
> There are also a couple of key differences with regards to the tag and 
> ABI that will make value type exceptions aka zero cost exceptions work 
> fairly fast.
> 
> ...
> 
> ```d
> sumtype Nullable(T) {
>      :none,
>      T value
> }
> 
> sumtype Nullable(T) = :none | T value;
> 
> void accept(Nullable!Duration timeout) {}
> 
> accept(1.minute);
> accept(:value = 1.minute);
> accept(:none);
> ```
> ...
> 
> The first comes from Walter Bright's proposal:
> 
> ```d
> sumtype Identifier (TemplateParameters) {
>      @UDAs|opt Type Identifier = Expression,
>      @UDAs|opt Type Identifier,
>      @UDAs|opt MemberOfOperator,
> }
> ```
> ...

> TODO: swap for spec grammar version
> 
> The second is short hand which comes from the ML family:
> 
> ```d
> sumtype Identifier (TemplateParameters) = @UDAs|opt Type Identifier|opt 
> | @UDAs|opt MemberOfOperator;
> ```
> 
> TODO: swap for spec grammar version
> 
> For a nullable type this would look like in both syntaxes:
> 
> ```d
> sumtype Nullable(T) {
>      :none,
>      T value
> }
> 
> sumtype Nullable(T) = :none | T value;
> ```
> ...

I have to say, I am not a big fan of having only parameterless named 
constructors as a special case.


> Implicit construction and applies to types in general in any location including function arguments.
> 
> ## Storage
> 
> A sumtype at runtime is represented by a flexible ABI.
> 
> 1. The tag [``size_t``]
> 2. Copy constructor [``function``]
> 3. Destructor [``function``]
> 4. Storage [``void[X]``]
> ...

It is not so clear why copy constructor and destructor need to be 
virtual functions.

> 
> The default initialization of a sumtype will always prefer ``:none`` if 
> present, otherwise it is the first entry.

Just do first entry always.

> For the first entry on the short hand syntax it does not support 
> expressions for the default initialization, therefore it will be the 
> default initialized value of that type.
> ...

In this case, I am not sure what initializers inside a sumtype will do.

> Assigning a value to a sum type, will always prefer the currently 
> selected tag.

Alarm bells go off in my head.

> If however the value cannot be coerced into the tag's type, it will then 
> do a match to determine the best candidate based upon the type of the 
> expression.
> 
> An example of prefering the currently selected tag:
> 
> ```d
> sumtype S = int i | long l;
> 
> S s = :i = 2;
> ```
> ...

I would strongly advise to drop this.

> But if we switch to a larger value ``s = long.max;``, this will assign 
> the long instead.
> 
> ## Nullability
> 
> A sum type cannot have the type state of null.
> ...

I am not sure what that means.

> ## Set Operations
> 
> A sumtype which is a subset of another, will be assignable.
> 
> ```d
> sumtype S1 = :none | int;
> sumtype S2 = :none | int | float;
> 
> S1 s1;
> S2 s2 = s1;
> ```
> ...

This seems like a strange mix of nominal and structural typing.

> This covers other scenarios like returning from a function or an 
> argument to a function.
> 
> To remove a possible entry from a sumtype you must peform a match (which 
> is not being proposed here):
> 
> ```d
> sumtype S1 = :none | int;
> sumtype S2 = :none | int | float;
> 
> S1 s1;
> S2 s2 = s1;
> 
> s2.match {
>      (float) => assert(0);
>      (default val) s1 = val;
> }
> ```
> 
> To determine if a type is in the set:
> 
> ```d
> sumtype S1 = :none | int;
> 
> pragma(msg, int in S1); // true
> pragma(msg, :none in S1); // true
> pragma(msg, "none" in S1); // true
> ```
> ...

I think a priori here you will have an issue with parsing.

> To merge two sumtypes together use the pipe operator on the type.
> 
> ```d
> sumtype S1 = :none | int i;
> sumtype S2 = :none | long l;
> alias S3 = S1 | S2; // :none | int i | long l
> ```
> ...

The flattening behavior is unintuitive.


> Or you can expand a sumtype directly into another:
> 
> ```d
> sumtype S1 = :none | int i;
> sumtype S2 = :none | S1.expand | long l; // :none | int i | long l
> ```
> 
> When merging, duplicate types and names are not an error, they will be 
> combined.
> Although if two names have different types this will error.
> ...

Again this mixes nominal and structural typing in a way that seems 
unsatisfying. Note that different struct types do not become assignable 
just because they share member types and names.



More information about the Digitalmars-d mailing list