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
Source std/sumtype.d
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:
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.
Introspection-based matching
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:
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.
Arithmetic expression evaluator
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 usingpattern 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. ASumType
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.See Also:- 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)(Valuevalue
)
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
(Trhs
); - 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: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.
Memory corruption
SumType!(int*, int) s = new int; s.tryMatch!( (ref int* p) { s = 123; // overwrites `p` return *p; // undefined behavior } );
- ref SumType
opAssign
(ref SumTyperhs
); - 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
(SumTyperhs
); - 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 Rhsrhs
)
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 Sinksink
, const ref FormatSpec!Charfmt
); - 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. See Also: - 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 theSumType
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 multipleSumType
s 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.See Also:Examples:Sometimes, implicit conversions may cause a handler to match more types than intended. The example below shows two solutions to this problem.Avoiding unintentional matches
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: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 toMultiple dispatch
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 SumTypesargs
)
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 asmatch
, 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.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.See Also:- ref auto
tryMatch
(SumTypes...)(auto ref SumTypesargs
)
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
, stringfile
= __FILE__, size_tline
= __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));
Copyright © 1999-2024 by the D Language Foundation | Page generated by
Ddoc on Sat Oct 12 23:29:37 2024