Thread overview
First Draft: Callback For Matching Type
Jun 27
Dom DiSc
Jun 26
ryuukk_
June 22

This proposal is a subset of matching capabilities to allow for tagged unions to safely access with language support its values and handle each tag.

Some minor things have been changed from the ideas thread, I have changed the match block to be a declaration block to allow for static foreach and other conditional compilation language features. So it is now using semicolon instead of colon.

alias MTU = MyTaggedUnion!(int, float, string);

MTU mtu = MTU(1.5);

mtu.match {
	(float v) => writeln("a float! ", v);
	v => writeln("catch all! ", v);
};

Ideas thread: https://forum.dlang.org/post/chzxzjiwsxmvnkthbdyy@forum.dlang.org

Latest: https://gist.github.com/rikkimax/79cbe199618b3f99104f7df2fc2a9681

Permanent: https://gist.github.com/rikkimax/79cbe199618b3f99104f7df2fc2a9681/95ae646da1ebb079a522b0c993e3408e5a1c0d78

June 24

On Saturday, 22 June 2024 at 21:02:34 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

This proposal is a subset of matching capabilities to allow for tagged unions to safely access with language support its values and handle each tag.

Some minor things have been changed from the ideas thread, I have changed the match block to be a declaration block to allow for static foreach and other conditional compilation language features. So it is now using semicolon instead of colon.

alias MTU = MyTaggedUnion!(int, float, string);

MTU mtu = MTU(1.5);

mtu.match {
	(float v) => writeln("a float! ", v);
	v => writeln("catch all! ", v);
};

Ideas thread: https://forum.dlang.org/post/chzxzjiwsxmvnkthbdyy@forum.dlang.org

Latest: https://gist.github.com/rikkimax/79cbe199618b3f99104f7df2fc2a9681

Permanent: https://gist.github.com/rikkimax/79cbe199618b3f99104f7df2fc2a9681/95ae646da1ebb079a522b0c993e3408e5a1c0d78

I guess I have implemented something like that: https://d.godbolt.org/z/ePv4ndxeE

If I understand you correctly, we share the vision of a tagged union (I call them enum unions) as a type with certain members (duck typing), not the instance of a particular template.

But that’s where it seems our views diverge. In my implementation, the tag also allows distinct same-type options. (Options are discerned by tag, not by type.)

A type with the appropriate members is (usually) generated by mixing in a given mixin template (EnumUnion) which takes one parameter of struct type (usually a small private struct named Impl) and uses its data members (types and names) for types and tags. (I used to have EnumUnion take an array of string for names and a type tuple for types, but those get really long really fast and error messages become incomprehensible name–type gibberish.)

Example time! Let’s say we want simple expression parsing where an expression is a constant, a variable, a unary minus expression, or a binary plus or times expression.

class Expr
{
    struct Binary { Expr lhs, rhs; }
    private static struct Impl
    {
        int constant;
        string variable;
        Expr minus;
        Binary plus, times;
    }
    mixin EnumUnion!Impl;
    // Provides: Constructors, a destructor if needed (not this case),
    // eponymous accessors (@safe get and @system set), @system re-assignment,
    // and some other stuff with two underscores in front.
}

Accessors:

  • constant, variable, etc. getters return the constant/variable/… if the option is active, otherwise assert(0) with error message.
  • constant, variable, etc. setters make the constant/variable/… option active and assign a value. (@system)

Among the other stuff:

  • __is_constant, __is_variable, etc. return a boolean if the option is active.
  • __as_constant, __as_variable, etc. return a pointer to the mentioned option if it’s active, or null. Essentially a safe cast. Similar to key in aa for associative array lookup.
  • __unsafe_constant, __unsafe_variable, etc. return a reference, checked by an in contract. (@system)

We’re not done! Because enum unions aren’t simply instances of a template, but just duck-typed stuff, enum union types can be classes or structs depending on your needs and can have additional members!

class Expr
{
    …

    int eval(int[string] context) => this.matchOrdered!(
        (constant) => constant,
        (variable) => context[variable],
        (minus)    => -minus.eval(context),
        (plus)     => plus.lhs.eval(context) + plus.rhs.eval(context),
        (times)    => times.lhs.eval(context) * times.rhs.eval(context),
    );
}

What is matchOrdered? A template defined in the same module as EnumUnion. It requires that all cases be handled (no default/catch-all) and in order of tags, that is, if you swap (constant) => constant, and (variable) => context[variable], you get an error. You do get the error because the parameter and tag names don’t line up, not because of a coincidental type mismatch.

There is also match which also requires all cases be handled but in any order. Handlers are inspected for the names of their parameters, get reordered, and passed to matchOrdered. Generally, use matchOrdered as you get better diagnosis.

There are also matchOrderedDefault and matchDefault which consider their last argument a default/catch-all handler.

Tags are also used for construction (named parameters). If, by types, construction is ambiguous, a tag can be used to clarify:

void main() @safe
{
    // Build (-2) * 1 + (-x)
    immutable Expr expr = new Expr(plus: Expr.Binary(
        new Expr(times: Expr.Binary(
            new Expr(-2),
            new Expr(1)
        )),
        new Expr(minus: new Expr("x"))
    ));
    import std.stdio;
    writeln(expr, " = ", expr.eval(["x": 1]));
}

For plus and times, tags are required as they’re indistinguishable otherwise. For minus, the tag is optional, but helps understanding what’s built. For variables and constants, tags aren’t used in the example.

You could use enum unions to back sum types:

struct SumType(Ts...)
{
    private static struct Impl
    {
        static foreach (i, alias T; Ts)
            mixin("T field", cast(int) i, ";");
    }
    mixin EnumUnion!Impl;
}

From what I see, you want to make match an intrinsic, and TBH, the value of

x.match {
    // handlers
}

over

x.match!(
    // handlers
)

is negligible.

The value of being a first-class language construct is similar to the foreachopApply lowering: return and other control-flow statements in the handlers could get lowered some way. Allowing that for arbitrary lambdas would be powerful and essentially allow programmers to implement custom control-flow statements.

June 25
On 25/06/2024 6:20 AM, Quirin Schroll wrote:
> I guess I have implemented something like that: https://d.godbolt.org/z/ePv4ndxeE
> 
> If I understand you correctly, we share the vision of a tagged union (I call them enum unions) as a type with certain members (duck typing), not the instance of a particular template.
> 
> But that’s where it seems our views diverge. In my implementation, the tag also allows distinct same-type options. (Options are discerned by tag, not by type.)

I'm for this, however since I don't have member-of-operator I can't really do this in the way that I'd like.

```d
expr.match {
	(:NameForTag var) => writeln(var);
}
```

Best to leave such support for future, unless a good proposal is given.

June 26

On Monday, 24 June 2024 at 18:20:36 UTC, Quirin Schroll wrote:

>

From what I see, you want to make match an intrinsic, and TBH, the value of

x.match {
    // handlers
}

over

x.match!(
    // handlers
)

is negligible.

i disagree, it's that kind of things that makes Rust ugly, hard to read and as a result harder to learn, let's not repeat the same mistakes

June 26

On Monday, 24 June 2024 at 18:42:21 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

On 25/06/2024 6:20 AM, Quirin Schroll wrote:

>

I guess I have implemented something like that: https://d.godbolt.org/z/ePv4ndxeE

If I understand you correctly, we share the vision of a tagged union (I call them enum unions) as a type with certain members (duck typing), not the instance of a particular template.

But that’s where it seems our views diverge. In my implementation, the tag also allows distinct same-type options. (Options are discerned by tag, not by type.)

I'm for this, however since I don't have member-of-operator I can't really do this in the way that I'd like.

expr.match {
	(:NameForTag var) => writeln(var);
}

Best to leave such support for future, unless a good proposal is given.

I now have implemented a SumType that is commutative, associative, and idempotent, that is:

  • the order of types does not matter: SumType!(T, U) is the same as SumType!(U, T),
  • they auto-inline like alias sequences: SumType!(Ts, SumType!Us, SumType!Vs) is the same as SumType!(Ts, Us, Vs), and
  • duplicates don’t matter: SumType!(T, T, Ts) is SumType!(T, Ts).

Also, any noreturn is removed from a SumType, any SumType with void becomes void, and any 1-ary SumType is just an alias to that type.

Matching is done using one of the following functions:

  • matchByType: Every handler must match with exactly one option, possibly by conversion, otherwise error.
  • matchByTypeExact: For every option, there must be a handler with exactly that parameter type (possibly qualified).
  • matchByTypeDefault: Same as matchByType, but the last handler is applied to all options that couldn’t be handled by a type-recognized handler.
  • matchByTypeExactDefault: Same as matchByTypeExact, but the last handler is applied to all options that couldn’t be handled by a type-recognized handler.

Example

import std.stdio;

alias S1 = SumType!(int, string);
alias S2 = SumType!(long, string);
alias S = SumType!(S1, S2, bool);
static assert(is(S == SumType!(string, long, bool, int)));
auto x = S("abc");

auto h = x.matchByTypeExactDefault!(
    (const string s) => s.length,
    (bool b) => b ? 10 : 20,
    (long x) => cast(size_t)x,
);
writeln(h); // 3

The int and long option can’t be handled by the string or bool handler, thus the last handler is used.

Default handlers can be generic, but I’m getting a lot of dual-context deprecation warnings unless I do some tricks.

Try it out here: https://d.godbolt.org/z/ndzd1z44e

June 27

On Wednesday, 26 June 2024 at 17:14:24 UTC, Quirin Schroll wrote:

>

I now have implemented a SumType that is commutative, associative, and idempotent, that is:

  • the order of types does not matter: SumType!(T, U) is the same as SumType!(U, T),
  • they auto-inline like alias sequences: SumType!(Ts, SumType!Us, SumType!Vs) is the same as SumType!(Ts, Us, Vs), and
  • duplicates don’t matter: SumType!(T, T, Ts) is SumType!(T, Ts).

This is so cool.
I was always sceptical about the usefulness of sumtypes, but with these properties I actual like it.