SumType is a generic discriminated union implementation that usesdesign-by-introspection to generate safe and efficient code. Its featuresinclude:Pattern matching.Sourcestd/sumtype.d
import std.math.operations : isClose;struct Fahrenheit {double value; }struct Celsius {double value; }struct Kelvin {double value; }alias Temperature = SumType!(Fahrenheit, Celsius, Kelvin);// Construct from any of the member types.Temperature t1 = Fahrenheit(98.6);Temperature t2 = Celsius(100);Temperature t3 = Kelvin(273);// Use pattern matching to access the value.Fahrenheit toFahrenheit(Temperature t){return Fahrenheit( t.match!( (Fahrenheit f) => f.value, (Celsius c) => c.value * 9.0/5 + 32, (Kelvin k) => k.value * 9.0/5 - 459.4 ) );}assert(toFahrenheit(t1).value.isClose(98.6));assert(toFahrenheit(t2).value.isClose(212));assert(toFahrenheit(t3).value.isClose(32));// Use ref to modify the value in place.void freeze(ref Temperature t){ t.match!( (ref Fahrenheit f) => f.value = 32, (ref Celsius c) => c.value = 0, (ref Kelvin k) => k.value = 273 );}freeze(t1);assert(toFahrenheit(t1).value.isClose(32));// Use a catch-all handler to give a default result.bool isFahrenheit(Temperature t){return t.match!( (Fahrenheit f) =>true, _ =>false );}assert(isFahrenheit(t1));assert(!isFahrenheit(t2));assert(!isFahrenheit(t3));
string handle(int n) {return"got an int"; }string handle(string s) {return"got a string"; }string handle(double d) {return"got a double"; }alias handle = match!handle;Usage would look like this:
alias ExampleSumType = SumType!(int, string,double);ExampleSumType a = 123;ExampleSumType b ="hello";ExampleSumType c = 3.14;writeln(a.handle);// "got an int"writeln(b.handle);// "got a string"writeln(c.handle);// "got a double"
import std.functional : partial;import std.traits : EnumMembers;import std.typecons : Tuple;enum Op : string{ Plus ="+", Minus ="-", Times ="*", Div ="/"}// An expression is either// - a number,// - a variable, or// - a binary operation combining two sub-expressions.alias Expr = SumType!(double, string, Tuple!(Op,"op", This*,"lhs", This*,"rhs"));// Shorthand for Tuple!(Op, "op", Expr*, "lhs", Expr*, "rhs"),// the Tuple type above with Expr substituted for This.alias BinOp = Expr.Types[2];// Factory function for number expressionsExpr* num(double value){returnnew Expr(value);}// Factory function for variable expressionsExpr* var(string name){returnnew Expr(name);}// Factory function for binary operation expressionsExpr* binOp(Op op, Expr* lhs, Expr* rhs){returnnew Expr(BinOp(op, lhs, rhs));}// Convenience wrappers for creating BinOp expressionsalias sum = partial!(binOp, Op.Plus);alias diff = partial!(binOp, Op.Minus);alias prod = partial!(binOp, Op.Times);alias quot = partial!(binOp, Op.Div);// Evaluate expr, looking up variables in envdouble eval(Expr expr,double[string] env){return expr.match!( (double num) => num, (string var) => env[var], (BinOp bop) {double lhs = eval(*bop.lhs, env);double rhs = eval(*bop.rhs, env);finalswitch (bop.op) {staticforeach (op; EnumMembers!Op) {case op:returnmixin("lhs" ~ op ~"rhs"); } } } );}// Return a "pretty-printed" representation of exprstring pprint(Expr expr){import std.format : format;return expr.match!( (double num) =>"%g".format(num), (string var) => var, (BinOp bop) =>"(%s %s %s)".format( pprint(*bop.lhs),cast(string) bop.op, pprint(*bop.rhs) ) );}Expr* myExpr = sum(var("a"), prod(num(2), var("b")));double[string] myEnv = ["a":3,"b":4,"c":7];writeln(eval(*myExpr, myEnv));// 11writeln(pprint(*myExpr));// "(a + (2 * b))"
This;SumType.SumType(Types...) if (is(NoDuplicates!Types == Types) && (Types.length > 0));SumType can be operated on usingpattern matching. To avoid ambiguity, duplicate types are not allowed (but see the"basic usage" example for a workaround). The special typeThis can be used as a placeholder to create self-referential types, just like withAlgebraic. See the"Recursive SumTypes" example for usage. ASumType is initialized by default to hold the.init value of its first member type, just like a regular union. The version identifierSumTypeNoDefaultCtor can be used to disable this behavior.Types = AliasSeq!(ReplaceTypeUnless!(isSumTypeInstance, This, typeof(this), TemplateArgsOf!SumType));value);value) const;value) immutable;value) inoutother) inout;opAssign(Trhs);SumType!(int*,int) s =newint;s.tryMatch!( (refint* p) { s = 123;// overwrites `p`return *p;// undefined behavior });
opAssign(ref SumTyperhs);opAssign(SumTyperhs);opEquals(this This, Rhs)(auto ref Rhsrhs)toString(this This)();toString(this This, Sink, Char)(ref Sinksink, ref const FormatSpec!Charfmt);Sinksink | Output range to write to. |
FormatSpec!Charfmt | Format specifier to use. |
toHash() const;isSumType(T);SumType or implicitly converts to one, otherwise false.staticstruct ConvertsToSumType{ SumType!int payload;alias payloadthis;}staticstruct ContainsSumType{ SumType!int payload;}assert(isSumType!(SumType!int));assert(isSumType!ConvertsToSumType);assert(!isSumType!ContainsSumType);
match(handlers...)SumType.SumType can hold, the given handlers are checked, in order, to see whether they accept a single argument of that type. The first one that does is chosen as the match for that type. (Note that the first match may not always be the most exact match. See"Avoiding unintentional matches" for one common pitfall.) Every type must have a matching handler, and every handler must match at least one type. This is enforced at compile time. Handlers may be functions, delegates, or objects withopCall overloads. If a function with more than one overload is given as a handler, all of the overloads are considered as potential matches. Templated handlers are also accepted, and will match any type for which they can beimplicitly instantiated. (Remember that afunction literal without an explicit argument type is considered a template.) If multipleSumTypes are passed to match, their values are passed to the handlers as separate arguments, and matching is done for each possible combination of value types. See"Multiple dispatch" for an example.alias Number = SumType!(double,int);Number x;// Problem: because int implicitly converts to double, the double// handler is used for both types, and the int handler never matches.assert(!__traits(compiles, x.match!( (double d) =>"got double", (int n) =>"got int" )));// Solution 1: put the handler for the "more specialized" type (in this// case, int) before the handler for the type it converts to.assert(__traits(compiles, x.match!( (int n) =>"got int", (double d) =>"got double" )));// Solution 2: use a template that only accepts the exact type it's// supposed to match, instead of any type that implicitly converts to it.alias exactly(T,alias fun) =function (arg){staticassert(is(typeof(arg) == T));return fun(arg);};// Now, even if we put the double handler first, it will only be used for// doubles, not ints.assert(__traits(compiles, x.match!( exactly!(double, d =>"got double"), exactly!(int, n =>"got int") )));
match, as show below.struct Point2D {double x, y; }struct Point3D {double x, y, z; }alias Point = SumType!(Point2D, Point3D);version (none){// This function works, but the code is ugly and repetitive.// It uses three separate calls to match! @safepurenothrow @nogcbool sameDimensions(Point p1, Point p2) {return p1.match!( (Point2D _) => p2.match!( (Point2D _) =>true, _ =>false ), (Point3D _) => p2.match!( (Point3D _) =>true, _ =>false ) ); }}// This version is much nicer.@safepurenothrow @nogcbool sameDimensions(Point p1, Point p2){alias doMatch =match!( (Point2D _1, Point2D _2) =>true, (Point3D _1, Point3D _2) =>true, (_1, _2) =>false );return doMatch(p1, p2);}Point a = Point2D(1, 2);Point b = Point2D(3, 4);Point c = Point3D(5, 6, 7);Point d = Point3D(8, 9, 0);assert( sameDimensions(a, b));assert( sameDimensions(c, d));assert(!sameDimensions(a, c));assert(!sameDimensions(d, b));
match(SumTypes...)(auto ref SumTypesargs)args.length > 0));match function.SumTypesargs | One or moreSumType objects. |
tryMatch(handlers...)SumType, and throws on failure.match, but are not required to be exhaustive—in other words, a type (or combination of types) is allowed to have no matching handler. If a type without a handler is encountered at runtime, aMatchException is thrown. Not available when compiled with-betterC.MatchException, if the currently-held type has no matching handler.tryMatch(SumTypes...)(auto ref SumTypesargs)args.length > 0));tryMatch function.SumTypesargs | One or moreSumType objects. |
MatchException:object.Exception;tryMatch when an unhandled type is encountered.msg, stringfile = __FILE__, size_tline = __LINE__);canMatch(alias handler, Ts...) if (Ts.length > 0)match for a full explanation of how matches are chosen.alias handleInt = (int i) =>"got an int";assert(canMatch!(handleInt,int));assert(!canMatch!(handleInt, string));
has(T)| T | the type to check for. |
SumType!(string,double) example ="hello";assert( example.has!string);assert(!example.has!double);// If T isn't part of the SumType, has!T will always return false.assert(!example.has!int);
alias Example = SumType!(string,double);Example m ="mutable";const Example c ="const";immutable Example i ="immutable";assert( m.has!string);assert(!m.has!(const(string)));assert(!m.has!(immutable(string)));assert(!c.has!string);assert( c.has!(const(string)));assert(!c.has!(immutable(string)));assert(!i.has!string);assert(!i.has!(const(string)));assert( i.has!(immutable(string)));
import std.algorithm.iteration : filter;import std.algorithm.comparison : equal;alias Example = SumType!(string,double);auto arr = [ Example("foo"), Example(0), Example("bar"), Example(1), Example(2), Example("baz")];auto strings = arr.filter!(has!string);auto nums = arr.filter!(has!double);assert(strings.equal([Example("foo"), Example("bar"), Example("baz")]));assert(nums.equal([Example(0), Example(1), Example(2)]));
has(Self)(auto ref Selfself)has function.Selfself | theSumType to check. |
self contains aT, otherwise false.get(T)has to check.| T | the type of the value being accessed. |
SumType!(string,double) example1 ="hello";SumType!(string,double) example2 = 3.14;writeln(example1.get!string);// "hello"writeln(example2.get!double);// 3.14
alias Example = SumType!(string,double);Example m ="mutable";const(Example) c ="const";immutable(Example) i ="immutable";writeln(m.get!string);// "mutable"writeln(c.get!(const(string)));// "const"writeln(i.get!(immutable(string)));// "immutable"
import std.algorithm.iteration : map;import std.algorithm.comparison : equal;alias Example = SumType!(string,double);auto arr = [Example(0), Example(1), Example(2)];auto values = arr.map!(get!double);assert(values.equal([0, 1, 2]));
get(Self)(auto ref Selfself)get function.Selfself | theSumType whose value is being accessed. |
tryGet(T)| T | the type of the value being accessed. |
SumType!(string,double) example ="hello";writeln(example.tryGet!string);// "hello"double result =double.nan;try result = example.tryGet!double;catch (MatchException e) result = 0;// Exception was thrownwriteln(result);// 0
import std.exception : assertThrown;const(SumType!(string,double)) example ="const";// Qualifier mismatch; throws exceptionassertThrown!MatchException(example.tryGet!string);// Qualifier matches; no exceptionwriteln(example.tryGet!(const(string)));// "const"
import std.algorithm.iteration : map, sum;import std.functional : pipe;import std.exception : assertThrown;alias Example = SumType!(string,double);auto arr1 = [Example(0), Example(1), Example(2)];auto arr2 = [Example("foo"), Example("bar"), Example("baz")];alias trySum = pipe!(map!(tryGet!double), sum);writeln(trySum(arr1));// 0 + 1 + 2assertThrown!MatchException(trySum(arr2));
tryGet(Self)(auto ref Selfself)tryGet function.Selfself | theSumType whose value is being accessed. |