Skip to content

Instantly share code, notes, and snippets.

@rikkimax
Last active February 17, 2024 10:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rikkimax/d25c6b2bed8caba008a6967e9e0a7e7c to your computer and use it in GitHub Desktop.
Save rikkimax/d25c6b2bed8caba008a6967e9e0a7e7c to your computer and use it in GitHub Desktop.

Depends upon: member of operator

SumTypes

Sum types are a union of types, as well as a union of names. Some names will be applied to a type, others may not be.

It acts as a tagged union, using a tag to determine which type or name is currently active.

The matching capabilities are not specified here.

It is influenced from Walter Bright's DIP, although it is not a continuation of.

Syntax

Two new declaration syntaxes are proposed.

The first comes from Walter Bright's proposal:

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:

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:

sumtype Nullable(T) {
    :none,
    T value
}

sumtype Nullable(T) = :none | T value;

Member Of

A sumtype is a kind of tag union. This uses a tag to differentiate between each member. The tag is a hash of both the fully qualified name of the type and the name. The usage of a hash for the tag is chosen to allow combining of sumtypes to be as cheap as a copy at runtime, without necessitating a match generated by the compiler to reassign the value.

The tag should be stored in a CPU word size register, so that if only names and no types are provided, there will be no storage.

When the member of operator applies to a sumtype it will locate given the member of identifier from the list of names the entry.

Proposed Match Parameters

There are two forms that need to be supported.

Both of which support a following name identifier that will be used for the variable declaration in the given scope.

  1. The first is a the type
  2. Second is the member of operator to match the name

It is recommended that if you can have conflicts to always declare entries with names and to always use the names in the matching.

obj.match {
    (:entry varName) => writeln(varName);
}

If you did not specify a type, you may not use the renamed variable declaration for a given entry nor specify the entry by the type.

It will of course be possible to specify an entry based upon the member of operator.

sumtype S = :none;

identity(:none);

S identity(S s) => return s;

As a feature this is overwise known as 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]]

The tag always exists.

If none of the entries has a copy constructor (including generated), this field does not exist.

If none of the entires has a destructor (including generated), this field does not exist.

If none of the entries takes any storage (so all entries do not have a type), this field does not exist.

Copy constructors and destructors for the entries that do not provide one, but are needed will have a generated internal to object file function generated that will perform the appropriete action (and should we get reference counting also perform that).

An alternative design to supporting a copy constructor and destructor function pointers is to match everytime a copy or destruction is required. Then do the operation on a per element basis. This however would be quite expensive to have to do often, and when you are already using enough storage (more than zero bytes) you have spilled the sumtype on to the stack, so two extra pointers is a cheap concession for improved performance.

For all intents and purposes a sum type is similar to a struct as far as when to call the copy constructors and destructors.

Initialization & Assignment

The default initialization of a sumtype will be the first entry, regardless of an initialization expression, types or names involved.

To assign to a sumtype variable without a name provided, only one entry may match that is unnamed. If multiple unnamed entries match it is an error and a name must be used instead. Coercion will be attempted if none match.

For named assignment it is of the form s.member = expression;. It will perform normal destruction followed with copying behavior similar to structs. This may work in @safe code.

For named initialization use the constructor call syntax S(Member: expression) or S("Member": expression), the latter supports CTFE'able expressions that evaluate out to a string.

Nullability

A sumtype cannot have the type state of null.

You cannot compare a sumtype to null.

Set Operations

A sumtype which is a subset of another, will be assignable.

sumtype S1 = :none | int;
sumtype S2 = :none | int | float;

S1 s1;
S2 s2 = s1;

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):

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:

sumtype S1 = :none | int;

pragma(msg, int in S1); // true
pragma(msg, :none in S1); // true
pragma(msg, "none" in S1); // true

To merge two sumtypes together use the expand property in a sumtype declaration.

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.

Introspection

A sumtype includes all primary properties of types including sizeof.

It has one new property, expand. Which is used to expand a sumtype into the currently declaring one.

The trait allMembers will return a set of strings that donate the names of each entry. If an entry has not been given a name by the user, a generated name will provided that will access it instead.

Using the trait getMember or using SumType.member will return an alias to that entry so that you may acquire the type of it, or to assign to it. Using either method to retrieve the value will automatically insert an assert to check the tag. Assignment using this method will be a regular named assignment as above. As this has the possibility of erroring at runtime, neither may be used in @safe code.

For the trait identifier on an alias of the a given entry, it will return the name for that entry.

An is expression may be used to determine if a given type is a sumtype: is(T == sumtype).

Comparison

The comparison of two sum types is first done based upon tag, if they are not equal that will give the less than and more than values.

Should they align, then a match will occur with the behavior for the given entry type resulting in the final comparison value. If a given entry does not have a type, then it will return as equal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment