First Draft: Callback For Matching Type

Quirin Schroll qs.il.paperinik at gmail.com
Mon Jun 24 18:20:36 UTC 2024


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.
>
> ```d
> 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.

```d
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!

```d
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:

```d
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:
```d
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
```d
x.match {
     // handlers
}
```
over
```d
x.match!(
     // handlers
)
```
is negligible.

The value of being a first-class language construct is similar to 
the `foreach` → `opApply` 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.


More information about the dip.development mailing list