Tutorial
Tutorial
Introduction
beman.monadics provides four monadic operations — and_then, transform, or_else, transform_error — as free functions that work uniformly across any type that models a "box": a container that either holds a value or signals absence/failure.
#include <beman/monadics/monadics.hpp>
namespace bms = beman::monadics;
All code examples in this document assume this setup, plus the relevant standard library header for the box type being demonstrated.
Opting In a Type
Opting a type into the box abstraction is explicit: you must specialize beman::monadics::box_traits<T>, even if the type already exposes a fully compatible interface. This prevents types from being silently treated as boxes with incorrect behaviour.
An empty specialization is the minimum required opt-in. Once opted in, any members the type already exposes are deduced automatically; you only need to supply what is missing.
Case 1: Minimal opt-in (well-structured types)
If the type already exposes a compatible interface (has_value(), value(), error(), nested value_type / error_type, and rebind / rebind_error), only the members the deduction cascade cannot find need to be provided.
std::expected is the canonical example. It already exposes has_value(), value(), error(), value_type, error_type, and its template parameters provide rebind and rebind_error. However, make_error cannot be deduced: std::expected constructs error states via std::unexpect-tagged construction, not from the error type directly. One member suffices:
#include <beman/monadics/monadics.hpp>
#include <expected>
#include <string>
template <typename T, typename E>
struct beman::monadics::box_traits<std::expected<T, E>> {
[[nodiscard]] static constexpr auto make_error(auto&& e) {
return std::expected<T, E>{std::unexpect, std::forward<decltype(e)>(e)};
}
};
using E = std::expected<int, std::string>;
auto result =
E{42}
| bms::and_then([](int v) { return E{v * 2}; })
| bms::transform([](int v) { return v + 1; })
| bms::or_else([](const std::string&){ return E{0}; })
| bms::transform_error([](auto e) { return "error: " + e; });
Case 2: Partial customization
If the type is mostly well-structured but misses one capability, provide only that piece. The rest is still deduced.
std::optional has has_value(), value(), and value_type, but has no error() member and no error_type. Provide those two things:
#include <beman/monadics/monadics.hpp>
#include <optional>
template <typename T>
struct beman::monadics::box_traits<std::optional<T>> {
[[nodiscard]] static constexpr auto error() { return std::nullopt; }
};
and_then, transform, and or_else now work. transform_error is automatically disabled because optional has no real error channel.
#include <optional>
#include <string>
auto parse = [](const std::string& s) -> std::optional<int> {
try { return std::stoi(s); } catch (...) { return {}; }
};
auto result =
std::optional<std::string>{"42"}
| bms::and_then(parse)
| bms::transform([](int v) { return v * 2; })
| bms::or_else([]() { return std::optional{0}; }); // optional{84}
std::shared_ptr is another partial case: it has no error(), no error_type, no rebind or rebind_error, but has_value and value can be wired up:
#include <beman/monadics/monadics.hpp>
#include <memory>
template <typename T>
struct beman::monadics::box_traits<std::shared_ptr<T>> {
[[nodiscard]] static bool has_value(const std::shared_ptr<T>& p) {
return static_cast<bool>(p);
}
[[nodiscard]] static decltype(auto) value(auto&& p) {
return *std::forward<decltype(p)>(p);
}
[[nodiscard]] static std::shared_ptr<T> make(T v) {
return std::make_shared<T>(std::move(v));
}
[[nodiscard]] static std::shared_ptr<T> error() { return nullptr; }
};
and_then, transform, and or_else work; transform_error is disabled.
#include <memory>
auto ptr = std::make_shared<int>(10);
auto result =
ptr
| bms::and_then([](int v) { return std::make_shared<int>(v * 2); })
| bms::transform([](int v) { return v + 3; })
| bms::or_else([]() { return std::make_shared<int>(0); }); // 23
Case 3: Fully custom type
For types that expose no compatible interface at all, provide the full set of static members.
#include <beman/monadics/monadics.hpp>
template <typename T>
struct Result {
bool ok;
T data;
int err;
};
template <typename T>
struct beman::monadics::box_traits<Result<T>> {
using value_type = T;
using error_type = int;
template <typename U> using rebind = Result<U>;
template <typename E> using rebind_error = Result<T>;
[[nodiscard]] static bool has_value(const Result<T>& r) { return r.ok; }
[[nodiscard]] static decltype(auto) value(auto&& r) {
return std::forward<decltype(r)>(r).data;
}
[[nodiscard]] static int error(const Result<T>& r) { return r.err; }
[[nodiscard]] static Result<T> make(T v) { return {true, std::move(v), 0}; }
[[nodiscard]] static Result<T> make_error(int e) { return {false, {}, e}; }
};
All four operations are now available:
constexpr Result<int> r{true, 10, 0};
constexpr auto result =
r
| bms::transform([](int v) { return v * 2; }) // Result{true, 20, 0}
| bms::and_then([](int v) { return Result<int>{true, v + 1, 0}; }) // Result{true, 21, 0}
| bms::transform_error([](int e) { return e + 100; }); // error unchanged (has value)
Operations Reference
and_then — flat-map over the value
Calls fn(value) when the box is non-empty; propagates absence/error unchanged. fn must return the same box template with the same error type.
#include <optional>
constexpr auto result = std::optional{42}
| bms::and_then([](int v) { return std::optional{v * 2}; }); // optional{84}
constexpr auto empty = std::optional<int>{}
| bms::and_then([](int v) { return std::optional{v * 2}; }); // nullopt
transform — map over the value
Calls fn(value) and rewraps the result. fn returns a plain value, not a box.
#include <optional>
constexpr auto result = std::optional{10}
| bms::transform([](int v) { return v + 1.5; }); // optional<double>{11.5}
or_else — recover from absence/error
Calls fn when the box is empty or in an error state; passes through the value unchanged. fn must return the same box template with the same value type.
For boxes without an error channel, fn takes no arguments:
#include <optional>
constexpr auto result = std::optional<int>{}
| bms::or_else([]() { return std::optional{0}; }); // optional{0}
For boxes with an error channel, fn receives the error:
#include <expected>
#include <string>
using E = std::expected<int, std::string>;
auto result = E{std::unexpected, "oops"}
| bms::or_else([](const std::string&) { return E{-1}; }); // expected{-1}
transform_error — map over the error
Calls fn(error) and replaces the error. Only available for box types with a real error channel (std::expected-like types; not std::optional or pointers).
#include <expected>
using E = std::expected<int, int>;
constexpr auto result = E{std::unexpected, 42}
| bms::transform_error([](int e) { return e * 2; }); // expected{unexpected, 84}
Pipe Composition
All operations compose with |:
#include <optional>
constexpr auto result =
std::optional{10}
| bms::and_then([](int v) { return std::optional{v * 2.0}; })
| bms::transform([](double v){ return static_cast<int>(v); })
| bms::and_then([](int) { return std::optional<char>{}; }) // fails here
| bms::or_else([]() { return std::optional{0}; }); // recovers
Compile-time Evaluation
All operations are constexpr. Any chain where the box type’s box_traits methods and all provided callables are also constexpr evaluates entirely at compile time. This works out of the box for std::optional and std::expected:
#include <beman/monadics/monadics.hpp>
#include <optional>
namespace bms = beman::monadics;
constexpr auto result =
std::optional{6}
| bms::and_then([](int v) { return std::optional{v * 7}; })
| bms::transform([](int v) { return v - 0; });
static_assert(result == std::optional{42});
Types that require runtime allocation (e.g. std::shared_ptr) remain runtime-only — the library imposes no restriction either way.
Writing Your Own Operations
Because box_traits and get_box_traits are public, you can write new operations that work across all adapted types without touching the library or the box types. See Design Rationale — Extensibility for details and examples.