ddn.types

ddn.types — reusable sum-type storage primitives.

This module provides the binary tagged union Either and the unit type None: the value storage that ddn.result.Result is built on, available on its own for new sum-typed APIs.

Either is a position-based sum: Either!(L, R) holds either an L (the left alternative) or an R (the right alternative); Either!(int, int) is legal because dispatch is by position, not by type. Construction, copying and inspection are allocation-free and non-throwing when the relevant payload operations are.

Semantics:

  • A default-constructed Either holds L.init in the left

    alternative: Either.init == Either.left(L.init).

  • Payload types are unconstrained apart from distinct positions;

    types equal after Unqual normalization remain legal (unlike ddn.result.Result, whose role selection needs distinct types).

  • Accessing the inactive alternative through Either.left or

    Either.right is a programming error. It terminates the process with a diagnostic on stderr — even in release builds — without allocating or throwing, exactly like the accessors of ddn.result.

  • Lifetime management is explicit and mirrors Phobos' SumType: a D

    union manages no member lifetimes, so copies, destruction and assignment act on the active member only. For plain-value payloads no explicit lifetime member is generated and Either stays POD.

Example:

import ddn.types : Either, match;

alias Text = Either!(string, Exception);

Text decode(bool good) @safe {
   if (good)
      return Text.right(new Exception("fallback"));
   return Text.left("malformed");
}

// Positional, exhaustive matching:
auto value = decode(false);
auto named = decode(true);
int code = value.match!(
   (text) => text.length,
   (error) => 1
);

Design record: doc/DDN-TYPES.md in the repository documents the storage, lifetime and safety decisions behind this module.

Types 3

structNone

The unit type: a value that carries no information.

D has no built-in unit type that can be stored as a field (empty structs force memory-class returns through unions, and typeof(null) is pointer-sized), so None reserves one byte instead. It compares equal to every other None and hashes consistently. Use it as an alternative payload when an Either branch needs no data, or wherever a "no value" marker distinct from every data type is required.

Fields
private ubyte _reservedReserved storage. The DMD-frontend x86-64 ABI classifier treats a union whose member is an empty struct as memory class; one byte keeps containing unions register-classifiable. It never carries inf...
structEither(L, R)

A value of exactly one of two types, distinguished by position.

Either!(L, R) privately stores a tagged union of L and R and adds unambiguous construction, tag queries, fail-fast payload access, structural equality and positional matching on top. See the module documentation for the lifetime and misuse contracts.

Parameters

Lthe left alternative's payload type.
Rthe right alternative's payload type.
Fields
private Storage _storage
private bool _isRightWhether the right alternative is active.
private _leftMisuseMsgDiagnostic for left-payload access on a right alternative.
private _rightMisuseMsgDiagnostic for right-payload access on a left alternative.
Methods
private _LeftBox!S _readLeft(S)(return ref S storage) if (is(S == Storage) || is(S == const(Storage)) || is(S == immutable(Storage)) || is(S == inout(Storage))) static ref pure nothrow @nogc @trustedRead the left alternative's storage without qualifier loss.
private _RightBox!S _readRight(S)(return ref S storage) if (is(S == Storage) || is(S == const(Storage)) || is(S == immutable(Storage)) || is(S == inout(Storage))) static ref pure nothrow @nogc @trustedRead the right alternative's storage without qualifier loss.
inout(R) _rightPayload() ref inout return pure nothrow @nogcPackage-level payload read for dispatching callers that have already checked the tag: the misuse diagnostic of `right()` is skipped, keeping this path pure like the exhaustive `match` built on it.
inout(L) _leftPayload() ref inout return pure nothrow @nogcPackage-level payload read for dispatching callers that have already checked the tag (see `rightPayload`).
inout(L) _payloadFor(bool right)() if (!right) ref inout returnSelect a payload reference by compile-time alternative.
inout(R) _payloadFor(bool right)() if (right) ref inout returnDitto.
Either left(T)(auto ref T value) if (is(T == L)) staticConstruct an `Either` holding the left alternative.
Either right(T)(auto ref T value) if (is(T == R)) staticConstruct an `Either` holding the right alternative.
private inout(Storage) _copiedLeftInout()(ref inout(typeof(this)) other) static @trustedCopy the storage's left alternative, preserving qualifiers.
private const(Storage) _copiedLeftConst()(ref const(typeof(this)) other) static @trustedDitto.
private immutable(Storage) _copiedLeftImmutable()( ref immutable(typeof(this)) other) static @trustedDitto.
private inout(Storage) _copiedRightInout()(ref inout(typeof(this)) other) static @trustedCopy the storage's right alternative, preserving qualifiers.
private const(Storage) _copiedRightConst()(ref const(typeof(this)) other) static @trustedDitto.
private immutable(Storage) _copiedRightImmutable()( ref immutable(typeof(this)) other) static @trustedDitto.
typeof(this) opAssign()(typeof(this) rhs) refReplace the active alternative by moving `rhs`'s in.
bool isLeft() const @safe pure nothrow @nogcReports whether the left alternative is active.
bool isRight() const @safe pure nothrow @nogcReports whether the right alternative is active.
inout(L) left() ref inout returnReturns the left payload by reference.
inout(R) right() ref inout returnReturns the right payload by reference.
bool opEquals(this This, Rhs)(auto ref Rhs rhs)Compares two eithers for equality.
size_t toHash() constHashes the active alternative and its tag.
private void _destroyActive() @trustedDestroys the active member only, leaving it at its `.init`.
private typeof(this) _replaceLeft()(L value) refReplace the active alternative with `value` as the left one.
private typeof(this) _replaceRight()(R value) refReplace the active alternative with `value` as the right one.
private void _misuse(string msg)() static @nogc nothrow @trustedTerminates the process after writing a compile-time-constant diagnostic.
Nested Templates
StorableField wrapper that keeps union storage default-declarable for payloads that disable default construction: the explicit `T.init` initializer is the same static initializer the field would receive im...
_LeftBox(S)Qualifier-preserving left-box type of a storage type: `Storable!L` under `S`'s qualifier (mutable, const, immutable or inout).
_RightBox(S)Qualifier-preserving right-box type of a storage type.
private structSuccessLike

Success-like single-reserved-byte payload for layout asserts.

Fields
private ubyte _reserved

Functions 2

fnauto _dispatchExact(string context, T, handlers...)(ref T payload)Invoke the handler whose sole parameter exactly matches `T`, ignoring top-level qualifiers. This is the type-dispatch core shared by `ddn.types.match` callers and `ddn.result.match`: exact dispatch...
private fnauto _dispatchExactAt(string context, size_t index, T, handlers...)(ref T payload)Recursive implementation for `dispatchExact`.

Variables 1

enumvarisPlainPayload = isCopyable!T && !hasElaborateCopyConstructor!T && !hasElaborateDestructor!T && !hasElaborateAssign!T

Internal fast-path predicate: true when values of T can be stored, copied, assigned and destroyed without any explicit lifetime management.

Plain payloads keep Either POD (register-returnable on x86-64) and use the compiler-generated copy, assignment and destruction. This is an implementation detail of ddn.types, not a public constraint: elaborate payloads are equally supported through the explicit lifetime members.

Parameters

Tpayload type to inspect.

Templates 2

tmpl_dispatchPayload(alias handler)

Package dispatch core: invoke handler with the active payload by reference, under the either's qualifier.

One free callback mirrors the storage-dispatch shape ddn.result.match is built on: the callback wraps the exact-type dispatch and captures only template aliases, so handler closures stay allocation-friendly.

Parameters

eithereither whose active payload is handed over, under any qualifier.
handlercallable accepting the active payload by reference.

Returns

whatever handler returns.
Functions
auto _dispatchPayload(E)(ref E either) if (is(Unqual!E == Either!(L, R), L, R))

Ditto.

tmplmatch(handlers...)

Exhaustive positional handling over both alternatives of an Either.

This is a free UFCS function so handlers may capture their enclosing scope. Selective imports must include match: import ddn.types : Either, match;. Attributes depend on the handlers; capturing delegates can require GC allocation.

Handlers are matched by position: the first handler receives the left payload, the second the right payload, so Either!(int, int) works. A handler is accepted when it is callable with its position's payload type. Only lvalue eithers are accepted: store a temporary in a named variable before matching (same rule as ddn.result.match).

Parameters

handlersexactly two callables: onLeft then onRight.
eitherlvalue either to inspect by reference without copying.

Returns

the active handler's return value, or void for void handlers.

Throws

any exception thrown by the handler or its payload operations.
Functions
auto match(E)(ref E either) if (is(Unqual!E == Either!(L, R), L, R))

Ditto.