expected

This module is implementing the Expected idiom.

See the Andrei Alexandrescu’s talk (Systematic Error Handling in C++ and its slides.

Or more recent "Expect the Expected" by Andrei Alexandrescu for further background.

It is also inspired by C++'s proposed std::expected and Rust's Result.

Similar work is expectations by Paul Backus.

Main features

  • lightweight, no other external dependencies
  • works with pure, @safe, @nogc, nothrow, and immutable
  • provides methods: ok, err, consume, expect, expectErr, andThen, orElse, map, mapError, mapOrElse
  • type inference for ease of use with ok and err
  • allows to use same types for T and E
  • allows to define Expected without value (void for T) - can be disabled with custom Hook
  • provides facility to change the Expected behavior by custom Hook implementation using the Design by introspection paradigm.
  • can enforce result check (with a cost)
  • can behave like a normal Exception handled code by changing the used Hook implementation
  • range interface

Description

Actual Expected type is defined as Expected!(T, E, Hook), where:

  • T defines type of the success value
  • E defines type of the error
  • Hook defines behavior of the Expected

Default type for error is string, i.e. Expected!int is the same as Expected!(int, string).

Abort is used as a default hook.

Hooks

Expected has customizable behavior with the help of a third type parameter, Hook. Depending on what methods Hook defines, core operations on the Expected may be verified or completely redefined. If Hook defines no method at all and carries no state, there is no change in default behavior.

This module provides a few predefined hooks (below) that add useful behavior to Expected:

AbortFails every incorrect operation with a call to assert(0). It is the default third parameter, i.e. Expected!short is the same as Expected!(short, string, Abort).
ThrowFails every incorrect operation by throwing an exception.
AsExceptionWith this hook implementation Expected behaves just like regular Exception handled code.

That means when function returns expected value, it returns instance of Expected with a success value. But when it tries to return error, Exception is thrown right away, i.e. Expected fails in constructor.

RCAbortSimilar to Abort hook but uses reference counted payload instead which enables checking if the caller properly checked result of the Expected.

The hook's members are looked up statically in a Design by Introspection manner and are all optional. The table below illustrates the members that a hook type may define and their influence over the behavior of the Checked type using it. In the table, hook is an alias for Hook if the type Hook does not introduce any state, or an object of type Hook otherwise.

Hook memberSemantics in Expected!(T, E, Hook)
enableDefaultConstructorIf defined, Expected would have enabled or disabled default constructor based on it's bool value. Default constructor is disabled by default. opAssign for value and error types is generated if default constructor is enabled.
enableCopyConstructorIf defined, Expected would have enabled or disabled copy constructor based on it's bool value. It is enabled by default. When disabled, it enables automatic check if the result was checked either for value or error. When not checked it calls hook.onUnchecked if provided.
WARNING: As currently it's not possible to change internal state of const or immutable object, automatic checking would't work on these. Hopefully with __mutable proposal..
enableRefCountedPayloadSet Expected instances to use reference counted payload storage. It's usefull when combined with onUnchecked to forcibly check that the result was checked for value or error.
enableVoidValueDefines if Expected supports void values. It's enabled by default so this hook can be used to disable it.
onAccessEmptyValueIf value is accessed on unitialized Expected or Expected with error value, hook.onAccessEmptyValue!E(err) is called. If hook doesn't implement the handler, T.init is returned.
onAccessEmptyErrorIf error is accessed on unitialized Expected or Expected with value, hook.onAccessEmptyError() is called. If hook doesn't implement the handler, E.init is returned.
onUncheckedIf the result of Expected isn't checked, hook.onUnchecked() is called to handle the error. If hook doesn't implement the handler, assert is thrown.
Note that hook.enableCopyConstructor must be false or hook.enableRefCountedPayload must be true for checks to work.
onValueSethook.onValueSet!T(val) function is called when success value is being set to Expected. It can be used for loging purposes, etc.
onErrorSethook.onErrorSet!E(err) function is called when error value is being set to Expected. This hook function is used by AsException hook implementation to change Expected idiom to normal Exception handling behavior.

Members

Classes

Unexpected
class Unexpected(T)

An exception that represents an error value.

Functions

andThen
auto ref andThen(EX exp, VEX value)
auto ref andThen(EX exp, Args args)

Returns the error contained within the Expected _and then_ another value if there's no error. This function can be used for control flow based on Expected values.

err
Expected!(T, E, Hook) err(E err)

Creates an Expected object from an error value, with type inference.

expect
T expect(EX res, string msg)
T expect(EX res)

Unwraps a result, yielding the content of expected value. If there is none, or error value, it throws assert(0) with the provided message.

expectErr
E expectErr(EX res, string msg)
E expectErr(EX res)

Unwraps a result, yielding the content of an error value. If there is none, or success value, it throws assert(0) with the provided message.

ok
Expected!(T, E, Hook) ok(T value)
Expected!(void, E, Hook) ok()

Creates an Expected object from an expected value, with type inference.

orElse
U orElse(EX exp, U value)
auto ref orElse(EX exp, Args args)

Returns the value contained within the Expected _or else_ another value if there's an error. This function can be used for control flow based on Expected values.

Structs

Abort
struct Abort

Default hook implementation for Expected

AsException
struct AsException

Hook implementation that behaves like a thrown exception. It throws Exception right when the Expected with error is initialized.

Expected
struct Expected(T, E = string, Hook = Abort)

Expected!(T, E) is a type that represents either success or failure.

RCAbort
struct RCAbort

Hook implementation that behaves same as Abort hook, but uses refcounted payload instead, which also enables us to check, if the result was properly checked before it is discarded.

Throw
struct Throw

Hook implementation that throws exceptions instead of default assert behavior.

Templates

consume
template consume(alias fun, Hook = Abort)

Constructs Expected from the result of the provided function.

hasOnAccessEmptyError
template hasOnAccessEmptyError(Hook)

Template to determine if hook provides function called on empty error.

hasOnAccessEmptyValue
template hasOnAccessEmptyValue(Hook, E)

Template to determine if hook provides function called on empty value.

hasOnErrorSet
template hasOnErrorSet(Hook, T)

Template to determine if hook provides function called when error is set.

hasOnUnchecked
template hasOnUnchecked(Hook)

Template to determine if hook provides custom handler for case when the Expected result is not checked.

hasOnValueSet
template hasOnValueSet(Hook, T)

Template to determine if hook provides function called when value is set.

isCopyConstructorEnabled
template isCopyConstructorEnabled(Hook)

Template to determine if hook enables or disables copy constructor.

isDefaultConstructorEnabled
template isDefaultConstructorEnabled(Hook)

Template to determine if provided Hook enables default constructor for Expected

isRefCountedPayloadEnabled
template isRefCountedPayloadEnabled(Hook)

Template to determine if hook defines that the Expected storage should use refcounted state storage.

isVoidValueEnabled
template isVoidValueEnabled(Hook)

Template to determine if provided Hook enables void values for Expected

map
template map(alias op, Hook = Abort)

Applies a function to the expected value in an Expected object.

mapError
template mapError(alias op, Hook = Abort)

Applies a function to the expected error in an Expected object.

mapOrElse
template mapOrElse(alias valueOp, alias errorOp)

Maps a Expected<T, E> to U by applying a function to a contained value, or a fallback function to a contained error value.

Examples

Basic usage

1 auto foo(int i) {
2     if (i == 0) return err!int("oops");
3     return ok(42 / i);
4 }
5 
6 version (D_Exceptions)
7 {
8     auto bar(int i) {
9         if (i == 0) throw new Exception("err");
10         return i-1;
11     }
12 }
13 
14 // basic checks
15 assert(foo(2));
16 assert(foo(2).hasValue);
17 assert(!foo(2).hasError);
18 assert(foo(2).value == 21);
19 
20 assert(!foo(0));
21 assert(!foo(0).hasValue);
22 assert(foo(0).hasError);
23 assert(foo(0).error == "oops");
24 
25 // void result
26 assert(ok()); // no error -> success
27 assert(!ok().hasError);
28 // assert(err("foo").hasValue); // doesn't have hasValue and value properties
29 
30 version (D_Exceptions)
31 {
32     // expected from throwing function
33     assert(consume!bar(1) == 0);
34     assert(consume!bar(0).error.msg == "err");
35 }
36 
37 // orElse
38 assert(foo(2).orElse!(() => 0) == 21);
39 assert(foo(0).orElse(100) == 100);
40 
41 // andThen
42 assert(foo(2).andThen(foo(6)) == 7);
43 assert(foo(0).andThen(foo(6)).error == "oops");
44 
45 // map
46 assert(foo(2).map!(a => a*2).map!(a => a - 2) == 40);
47 assert(foo(0).map!(a => a*2).map!(a => a - 2).error == "oops");
48 
49 // mapError
50 assert(foo(0).mapError!(e => "OOPS").error == "OOPS");
51 assert(foo(2).mapError!(e => "OOPS") == 21);
52 
53 // mapOrElse
54 assert(foo(2).mapOrElse!(v => v*2, e => 0) == 42);
55 assert(foo(0).mapOrElse!(v => v*2, e => 0) == 0);

Advanced usage - behavior modification

import exp = expected;

// define our Expected type using Exception as Error values
// and Throw hook, which throws when empty value or error is accessed
template Expected(T)
{
    alias Expected = exp.Expected!(T, Exception, Throw);
}

// create wrappers for simplified usage of our Expected
auto ok(T)(T val) { return exp.ok!(Exception, Throw)(val); }
auto err(T)(Exception err) { return exp.err!(T, Throw)(err); }

// use it as normal
assert(ok(42) == 42);
assert(err!int(new Exception("foo")).orElse(0) == 0);
assertThrown(ok(42).error);
assertThrown(err!int(new Exception("bar")).value);

Meta

License

BSL-1.0

Authors

Tomáš Chaloupka