Report a bug
If you spot a problem with this page, click here to create a Bugzilla issue.
Improve this page
Quickly fork, edit online, and submit a pull request for this page. Requires a signed-in GitHub account. This works well for small changes. If you'd like to make larger changes you may want to consider using a local clone.

std.sumtype

SumType is a generic discriminated union implementation that uses design-by-introspection to generate safe and efficient code. Its features include:
  • Pattern matching.
  • Support for self-referential types.
  • Full attribute correctness (pure, @safe, @nogc, and nothrow are inferred whenever possible).
  • A type-safe and memory-safe API compatible with DIP 1000 (scope).
  • No dependency on runtime type information (TypeInfo).
  • Compatibility with BetterC.
License:
Boost License 1.0
Authors:
Paul Backus
Examples:

Basic usage

import std.math.operations : isClose;

struct Fahrenheit { double degrees; }
struct Celsius { double degrees; }
struct Kelvin { double degrees; }

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.degrees,
            (Celsius c) => c.degrees * 9.0/5 + 32,
            (Kelvin k) => k.degrees * 9.0/5 - 459.4
        )
    );
}

assert(toFahrenheit(t1).degrees.isClose(98.6));
assert(toFahrenheit(t2).degrees.isClose(212));
assert(toFahrenheit(t3).degrees.isClose(32));

// Use ref to modify the value in place.
void freeze(ref Temperature t)
{
    t.match!(
        (ref Fahrenheit f) => f.degrees = 32,
        (ref Celsius c) => c.degrees = 0,
        (ref Kelvin k) => k.degrees = 273
    );
}

freeze(t1);
assert(toFahrenheit(t1).degrees.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));
Examples:

Introspection-based matching

In the length and horiz functions below, the handlers for match do not specify the types of their arguments. Instead, matching is done based on how the argument is used in the body of the handler: any type with x and y properties will be matched by the rect handlers, and any type with r and theta properties will be matched by the polar handlers.
import std.math.operations : isClose;
import std.math.trigonometry : cos;
import std.math.constants : PI;
import std.math.algebraic : sqrt;

struct Rectangular { double x, y; }
struct Polar { double r, theta; }
alias Vector = SumType!(Rectangular, Polar);

double length(Vector v)
{
    return v.match!(
        rect => sqrt(rect.x^^2 + rect.y^^2),
        polar => polar.r
    );
}

double horiz(Vector v)
{
    return v.match!(
        rect => rect.x,
        polar => polar.r * cos(polar.theta)
    );
}

Vector u = Rectangular(1, 1);
Vector v = Polar(1, PI/4);

assert(length(u).isClose(sqrt(2.0)));
assert(length(v).isClose(1));
assert(horiz(u).isClose(1));
assert(horiz(v).isClose(sqrt(0.5)));
Examples:

Arithmetic expression evaluator

This example makes use of the special placeholder type This to define a recursive data type: an abstract syntax tree for representing simple arithmetic expressions.
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 expressions
Expr* num(double value)
{
    return new Expr(value);
}

// Factory function for variable expressions
Expr* var(string name)
{
    return new Expr(name);
}

// Factory function for binary operation expressions
Expr* binOp(Op op, Expr* lhs, Expr* rhs)
{
    return new Expr(BinOp(op, lhs, rhs));
}

// Convenience wrappers for creating BinOp expressions
alias 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 env
double 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);
            final switch (bop.op)
            {
                static foreach (op; EnumMembers!Op)
                {
                    case op:
                        return mixin("lhs" ~ op ~ "rhs");
                }
            }
        }
    );
}

// Return a "pretty-printed" representation of expr
string 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)); // 11
writeln(pprint(*myExpr)); // "(a + (2 * b))"
struct This;
Placeholder used to refer to the enclosing SumType.
struct SumType(Types...) if (is(NoDuplicates!Types == Types) && (Types.length > 0));
A tagged union that can hold a single value from any of a specified set of types.
The value in a SumType can be operated on using pattern matching.
To avoid ambiguity, duplicate types are not allowed (but see the "basic usage" example for a workaround).
The special type This can be used as a placeholder to create self-referential types, just like with Algebraic. See the "Arithmetic expression evaluator" example for usage.
A SumType is initialized by default to hold the .init value of its first member type, just like a regular union. The version identifier SumTypeNoDefaultCtor can be used to disable this behavior.
alias Types = AliasSeq!(ReplaceTypeUnless!(isSumTypeInstance, This, typeof(this), TemplateArgsOf!SumType));
The types a SumType can hold.
this(T value);

const this(const(T) value);

immutable this(immutable(T) value);

inout this(Value)(Value value)
if (is(Value == DeducedParameterType!(inout(T))));
Constructs a SumType holding a specific value.
inout this(ref inout(SumType) other);
Constructs a SumType that's a copy of another SumType.
ref SumType opAssign(T rhs);
Assigns a value to a SumType.
If any of the SumType's members other than the one being assigned to contain pointers or references, it is possible for the assignment to cause memory corruption (see the "Memory corruption" example below for an illustration of how). Therefore, such assignments are considered @system.
An individual assignment can be @trusted if the caller can guarantee that there are no outstanding references to any SumType members that contain pointers or references at the time the assignment occurs.
Examples:

Memory corruption

This example shows how assignment to a SumType can be used to cause memory corruption in @system code. In @safe code, the assignment s = 123 would not be allowed.
SumType!(int*, int) s = new int;
s.tryMatch!(
    (ref int* p) {
        s = 123; // overwrites `p`
        return *p; // undefined behavior
    }
);
ref SumType opAssign(ref SumType rhs);
Copies the value from another SumType into this one.
See the value-assignment overload for details on @safety.
Copy assignment is @disabled if any of Types is non-copyable.
ref SumType opAssign(SumType rhs);
Moves the value from another SumType into this one.
See the value-assignment overload for details on @safety.
bool opEquals(this This, Rhs)(auto ref Rhs rhs)
if (!is(CommonType!(This, Rhs) == void));
Compares two SumTypes for equality.
Two SumTypes are equal if they are the same kind of SumType, they contain values of the same type, and those values are equal.
string toString(this This)();
Returns a string representation of the SumType's current value.
Not available when compiled with -betterC.
void toString(this This, Sink, Char)(ref Sink sink, const ref FormatSpec!Char fmt);
Handles formatted writing of the SumType's current value.
Not available when compiled with -betterC.
Parameters:
Sink sink Output range to write to.
FormatSpec!Char fmt Format specifier to use.
const size_t toHash();
Returns the hash of the SumType's current value.
Not available when compiled with -betterC.
enum bool isSumType(T);
True if T is a SumType or implicitly converts to one, otherwise false.
Examples:
static struct ConvertsToSumType
{
    SumType!int payload;
    alias payload this;
}

static struct ContainsSumType
{
    SumType!int payload;
}

assert(isSumType!(SumType!int));
assert(isSumType!ConvertsToSumType);
assert(!isSumType!ContainsSumType);
template match(handlers...)
Calls a type-appropriate function with the value held in a SumType.
For each possible type the 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 with opCall 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 be implicitly instantiated. See "Introspection-based matching" for an example of templated handler usage.
If multiple SumTypes 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.
Returns:
The value returned from the handler that matches the currently-held type.
Examples:

Avoiding unintentional matches

Sometimes, implicit conversions may cause a handler to match more types than intended. The example below shows two solutions to this problem.
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)
{
    static assert(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")
    )
));
Examples:

Multiple dispatch

Pattern matching can be performed on multiple SumTypes at once by passing handlers with multiple arguments. This usually leads to more concise code than using nested calls to 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!
    @safe pure nothrow @nogc
    bool 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.
@safe pure nothrow @nogc
bool 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));
ref auto match(SumTypes...)(auto ref SumTypes args)
if (allSatisfy!(isSumType, SumTypes) && (args.length > 0));
The actual match function.
Parameters:
SumTypes args One or more SumType objects.
template tryMatch(handlers...)
Attempts to call a type-appropriate function with the value held in a SumType, and throws on failure.
Matches are chosen using the same rules as 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, a MatchException is thrown.
Not available when compiled with -betterC.
Returns:
The value returned from the handler that matches the currently-held type, if a handler was given for that type.
Throws:
MatchException, if the currently-held type has no matching handler.
ref auto tryMatch(SumTypes...)(auto ref SumTypes args)
if (allSatisfy!(isSumType, SumTypes) && (args.length > 0));
The actual tryMatch function.
Parameters:
SumTypes args One or more SumType objects.
class MatchException: object.Exception;
Thrown by tryMatch when an unhandled type is encountered.
Not available when compiled with -betterC.
pure nothrow @nogc @safe this(string msg, string file = __FILE__, size_t line = __LINE__);
template canMatch(alias handler, Ts...) if (Ts.length > 0)
True if handler is a potential match for Ts, otherwise false.
See the documentation for match for a full explanation of how matches are chosen.
Examples:
alias handleInt = (int i) => "got an int";

assert( canMatch!(handleInt, int));
assert(!canMatch!(handleInt, string));