Thread overview
taggedalgebraic 0.11.0 adds TaggedUnion
Feb 22, 2019
Sönke Ludwig
Feb 23, 2019
Jacob Carlborg
Feb 23, 2019
Sönke Ludwig
Feb 23, 2019
Sönke Ludwig
Feb 23, 2019
Paul Backus
Feb 23, 2019
Sönke Ludwig
Feb 23, 2019
Sönke Ludwig
February 22, 2019
TaggedUnion is the low level tagged union functionality separated out from TaggedAlgebraic. Because it doesn't support transparent access of methods and operators of the contained value, it is able to provide a number of convenience features. On top of that, visit and tryVisit is now supported for pattern matching, analogous to the version for std.variant.Algebraic. This is the basic usage:

    union U {
        string caption;
        int height;
        int width;
    }
    alias Value = TaggedUnion!U;

    auto val = Value.caption("foo");
    assert(val.kind == Value.Kind.caption);
    assert(val.value!(Value.Kind.caption) == "foo");

    // shorthand syntax:
    assert(val.isCaption);
    assert(val.captionValue == "foo");

    // set a different type/kind
    val.setHeight(10);
    assert(val.isHeight && val.heightValue == 10);
    assert(!val.isWidth);

    assert(cast(short)val == 10);

    val.visit!(
        (int i) { assert(i == 10); }
        (string s) { assert(false); }
    );

    assert(val.visit!((v) => v.to!string) == "10");


Another addition is a new placeholder "This" type to enable defining self-referential types without having to write additional boilerplate code:

    union U {
        bool boolean;
        string text;
        float number;
        TaggedUnion!This[] array;
        TaggedUnion!This[string] object;
    }
    alias JsonValue = TaggedUnion!U;


The general advantages of TaggedUnion/TaggedAlgebraic over Algebraic result from its use of a numeric type tag instead of relying on runtime TypeInfo:

 - Allows compiler checked handling of all possible types/kinds using
   "final switch"
 - Enables inlining and function attribute inference (@safe, nothrow,
   @nogc)
 - Allows to use the same type with multiple tags to avoid the overhead
   of defining separate wrapper types
 - Behaves like a POD if only POD fields exist, is non-copyable if any
   contained type is non-copyable etc.


DUB: https://code.dlang.org/packages/taggedalgebraic
GitHub: https://github.com/s-ludwig/taggedalgebraic
February 23, 2019
On 2019-02-22 18:09, Sönke Ludwig wrote:
> TaggedUnion is the low level tagged union functionality separated out from TaggedAlgebraic. Because it doesn't support transparent access of methods and operators of the contained value, it is able to provide a number of convenience features. On top of that, visit and tryVisit is now supported for pattern matching, analogous to the version for std.variant.Algebraic. This is the basic usage:
> 
>      union U {
>          string caption;
>          int height;
>          int width;
>      }
>      alias Value = TaggedUnion!U;
> 
>      auto val = Value.caption("foo");
>      assert(val.kind == Value.Kind.caption);
>      assert(val.value!(Value.Kind.caption) == "foo");
> 
>      // shorthand syntax:
>      assert(val.isCaption);
>      assert(val.captionValue == "foo");
> 
>      // set a different type/kind
>      val.setHeight(10);

Why not using property syntax, i.e. `val.height = 10`?

>      val.visit!(
>          (int i) { assert(i == 10); }
>          (string s) { assert(false); }
>      );

How does this handle the above case when there are two `int`? How do I know if it's the "width" or "height" that has been set?

-- 
/Jacob Carlborg
February 23, 2019
Am 23.02.2019 um 09:51 schrieb Jacob Carlborg:
> On 2019-02-22 18:09, Sönke Ludwig wrote:
>> TaggedUnion is the low level tagged union functionality separated out from TaggedAlgebraic. Because it doesn't support transparent access of methods and operators of the contained value, it is able to provide a number of convenience features. On top of that, visit and tryVisit is now supported for pattern matching, analogous to the version for std.variant.Algebraic. This is the basic usage:
>>
>>      union U {
>>          string caption;
>>          int height;
>>          int width;
>>      }
>>      alias Value = TaggedUnion!U;
>>
>>      auto val = Value.caption("foo");
>>      assert(val.kind == Value.Kind.caption);
>>      assert(val.value!(Value.Kind.caption) == "foo");
>>
>>      // shorthand syntax:
>>      assert(val.isCaption);
>>      assert(val.captionValue == "foo");
>>
>>      // set a different type/kind
>>      val.setHeight(10);
> 
> Why not using property syntax, i.e. `val.height = 10`?

The main reason is to distinguish the act of setting a new value from the one of modifying an existing value. In the former case it is okay if the currently stored kind/type differs, but in the latter case that would be a programming error.

This would have worked with a setter and an r-value getter, but that would have unnecessarily impeded performance for working with complex types, at least for the primary access syntax.

Following from the above, the raw name is used instead as a static property to enable a short construction syntax for a specific kind (e.g. `Value.width(10)`).


>>      val.visit!(
>>          (int i) { assert(i == 10); }
>>          (string s) { assert(false); }
>>      );
> 
> How does this handle the above case when there are two `int`? How do I know if it's the "width" or "height" that has been set?
> 

I'm still thinking about a nice syntax for kind based pattern matching*, but for now this will call the same visitor for both, width and height. You'd have to query val.isWidth/val.kind to check the specific kind.


* really unfortunate that a template has to be instantiated first to be able to query the parameters of the contained function, otherwise `(@(Kind.width) i) { ... }` would be possible.
February 23, 2019
Am 23.02.2019 um 14:18 schrieb Sönke Ludwig:
> Am 23.02.2019 um 09:51 schrieb Jacob Carlborg:
>> (...)
>>
>> Why not using property syntax, i.e. `val.height = 10`?
> 
> The main reason is to distinguish the act of setting a new value from the one of modifying an existing value. In the former case it is okay if the currently stored kind/type differs, but in the latter case that would be a programming error.

To follow up on that, in the example above it would be possible to do this:

    val.setHeight(10);
    val.heightValue++;
    assert(val.heightValue == 11);

    //val.captionValue = "foo"; // throws an AssertError

Then there is also a generic variant of the above:

    val.set!(Value.Kind.height)(10);
    val.value!(Value.Kind.height)++;
    val.value!int++;
February 23, 2019
On Friday, 22 February 2019 at 17:09:41 UTC, Sönke Ludwig wrote:
>  - Behaves like a POD if only POD fields exist, is non-copyable if any
>    contained type is non-copyable etc.

In fact, if a contained type is non-copyable, it fails to compile altogether. (Example code below.)

Granted, so do Algebraic and SumType, so this isn't a knock against taggedalgebraic in particular. But it does indicate poor test coverage that you listed this feature without realizing it didn't work.

--- example.d
/+ dub.sdl:
dependency "taggedalgebraic" version="~>0.11.2"
+/
import taggedalgebraic;

struct NoCopy { @disable this(this); }
union U { NoCopy member; }
alias Test = TaggedUnion!U; // kaboom

void main() {}

February 23, 2019
Am 23.02.2019 um 20:19 schrieb Paul Backus:
> On Friday, 22 February 2019 at 17:09:41 UTC, Sönke Ludwig wrote:
>>  - Behaves like a POD if only POD fields exist, is non-copyable if any
>>    contained type is non-copyable etc.
> 
> In fact, if a contained type is non-copyable, it fails to compile altogether. (Example code below.)
> 
> Granted, so do Algebraic and SumType, so this isn't a knock against taggedalgebraic in particular. But it does indicate poor test coverage that you listed this feature without realizing it didn't work.
> 
> --- example.d
> /+ dub.sdl:
> dependency "taggedalgebraic" version="~>0.11.2"
> +/
> import taggedalgebraic;
> 
> struct NoCopy { @disable this(this); }
> union U { NoCopy member; }
> alias Test = TaggedUnion!U; // kaboom
> 
> void main() {}
> 

You are right, I was actually aware of some places where the value is not moved/swapped properly, but I forgot about it when rushing the post before leaving. The test coverage is actually pretty good, though, even if it is hard to give a meaningful number for template-heavy code like that.
February 23, 2019
Am 23.02.2019 um 21:46 schrieb Sönke Ludwig:
> Am 23.02.2019 um 20:19 schrieb Paul Backus:
>> On Friday, 22 February 2019 at 17:09:41 UTC, Sönke Ludwig wrote:
>>>  - Behaves like a POD if only POD fields exist, is non-copyable if any
>>>    contained type is non-copyable etc.
>>
>> In fact, if a contained type is non-copyable, it fails to compile altogether. (Example code below.)
>>
>> Granted, so do Algebraic and SumType, so this isn't a knock against taggedalgebraic in particular. But it does indicate poor test coverage that you listed this feature without realizing it didn't work.
>>
>> --- example.d
>> /+ dub.sdl:
>> dependency "taggedalgebraic" version="~>0.11.2"
>> +/
>> import taggedalgebraic;
>>
>> struct NoCopy { @disable this(this); }
>> union U { NoCopy member; }
>> alias Test = TaggedUnion!U; // kaboom
>>
>> void main() {}
>>
> 
> You are right, I was actually aware of some places where the value is not moved/swapped properly, but I forgot about it when rushing the post before leaving. The test coverage is actually pretty good, though, even if it is hard to give a meaningful number for template-heavy code like that.

https://github.com/s-ludwig/taggedalgebraic/pull/26