Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Design history of libfn, newest first. The living documents — [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), [docs/](docs/) — describe only the present state of the design; when a decision makes an earlier idea obsolete, this file is where the transition is recorded and explained.

## `&` takes a `void` side as the unit factor `pack<>` — 27 September 2026

`&` treats a `void` value as the unit factor `pack<>`. For example, `expected<int, E> & just<void>` yields `expected<pack<int>, E>`; previously it yielded `expected<int, E>`. This breaking change applies across carrier pairings in either order. Copack values distribute into packs, and `optional<T&> & just<void>` yields an owning `optional<pack<T>>`. Two `void` values still yield `void`.

## `pack::append` builds its result in place — 27 September 2026

`pack::append` constructs the final pack directly. Initializing its base from a temporary could otherwise relocate elements again and terminate if a move threw inside a `noexcept` call. Bracing each aggregate layer also prevents an element's conversion operator from initializing a whole layer in place of the element.

## `transform` over a copack maps a `void` result to `pack<>` — 27 September 2026

`transform` over a copack represents `void` callback results as `pack<>`. Mixed `void` and `int` results yield `copack_for<pack<>, int>`; all-`void` results yield `copack<pack<>>`. This applies through `expected`, `optional` and `choice`, and to `transform_error` over a copack error. Plain error types still reject `void` results. Mutable operands can select a `void` overload where they previously fell back to a valued `const` overload.
Expand Down
27 changes: 16 additions & 11 deletions TYPE_ALGEBRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ In `libfn`'s algebra, zero and unit are strictly separated:

Because `pack<>` exists, applying a callable to it invokes a nullary function. Because `copack<>` is uninhabited, providing a callback over `copack<>` is statically proven to be unreachable code (dead code).

In C++, `void` is often conflated with empty state, but algebraically, `void` is a unit type `1`, similar to `pack<>`.

Consider the difference in these carrier states:

| Computation | Meaning |
Expand Down Expand Up @@ -223,6 +221,14 @@ To invoke the algebra, you use the opt-in mechanisms provided by the library:

If a side is already a `copack` or `pack`, forwarding it behaves naturally without nesting. A `copack` on the error side of `expected` enables error-set unioning. Because monadic operations introduce no grade themselves, `and_then` widens a graded error side but rejects a differing ungraded one. Section 9 gives the exact promotion rules.

### The role of `void`

`void` represents success without a payload in `just<void>` and `expected<void, E>`. Algebraically, this is the unit `1`, like `pack<>`. Neither `pack` nor `copack` can hold `void` directly.

When an operation forms a `copack`, it represents a `void` result as `pack<>`. Joining only `void` branch values stays `void`; mixing them with other values introduces a `pack<>` alternative. Mapping a copack side always yields a copack, so even all-`void` callbacks produce `copack<pack<>>`.

In a conjunction, a `void` value contributes the unit factor `pack<>`. The value stays `void` only when both operands are `void`. On a plain side, `void` is valid as the value of `expected` or `just`, but not as the value of `optional` or the error of `expected`. Direct construction does not perform this conversion: `copack_for<void, T>` is ill-formed.

## 3. The computation carriers

To model computation and manage control flow (success, failure, alternatives, and empty states), `libfn` uses **computation carriers** (often called "monadic types"). The three carrier templates are `optional`, `expected` and `just`. The list below groups them by fallibility and payload; `choice` names the copack specialization of `just`:
Expand Down Expand Up @@ -401,7 +407,7 @@ Key principles of mapping:
- `transform` preserves the carrier family; its member form never leaves its own carrier type.
- Success and error states are preserved.
- A bare `copack` has a member `transform` to map across alternatives, but takes no pipeline functor as it is data, not a carrier.
- Heterogeneous results inside `transform` or `transform_error` form a normalized `copack`.
- Over a `copack` side, `transform` and `transform_error` yield a normalized `copack`, with `void` results represented as `pack<>`. Over a plain side, only `expected` and `just` accept a `void` value result.
- Applying `transform_error` to a carrier with no error side (such as `just` or `choice`) is ill-formed.
- When a side is uninhabited (`copack<>`), transformation is well-formed but vacuous: neither the member nor the pipeline form is reachable, and the callback is not instantiated. This applies to `optional<copack<>>` and `expected<copack<>, E>` on the value side, and `expected<T, copack<>>` on the error side.

Expand Down Expand Up @@ -471,7 +477,7 @@ An operand from the identity cluster (Section 10) contributes a value but never

- **Unchanged Errors**: Because identity cluster operands never fail, they add no alternatives to the error channel. A `just` or `choice` operand preserves the fallible operand's error side (plain or graded). An `expected<T, copack<>>` operand contributes its uninhabited grade to the error union: no active alternative is added, although the resulting error channel is promoted to a graded copack.
- **Value Bundling**: The identity cluster operand's value conjoins with the fallible operand's value into a `pack`.
- **Unit Elision**: `just<void>` and `expected<void, copack<>>` act as the product's identity unit and are elided from the value product (e.g., `expected<T, E> & just<void>` remains `expected<T, E>`).
- **Unit Factor**: a `void` operand, such as `just<void>` or `expected<void, copack<>>`, contributes the unit `pack<>` to the value product: `expected<T, E> & just<void>` is `expected<pack<T>, E>`, as `expected<T, E> & just<pack<>>` is. The value stays `void` only when every operand is `void`.
- **Choice Distribution**: Conjoining a `choice` with a fallible carrier distributes the coproduct through the product, yielding a `copack` of `pack`s wrapped in the fallible carrier.

<!-- sync-example-conjunction-with-identity-cluster -->
Expand All @@ -482,9 +488,8 @@ auto test_conjunction_with_identity_cluster(fn::expected<int, Error> ex, fn::jus
auto res1 = ex & j;
static_assert(std::same_as<decltype(res1), fn::expected<fn::pack<int, double>, Error>>);

// Conjoining with a unit (just<void>) completely elides the unit
auto res2 = ex & fn::just<void>{};
static_assert(std::same_as<decltype(res2), decltype(ex)>);
static_assert(std::same_as<decltype(res2), fn::expected<fn::pack<int>, Error>>);

// Conjoining a choice causes distribution inside the carrier
fn::choice<bool, double> ch = 1.5;
Expand All @@ -507,7 +512,7 @@ auto test_conjunction_with_identity_cluster(fn::expected<int, Error> ex, fn::jus
>
## 7. Sum composition with operator| (disjunction)

Disjunction evaluates alternative computations, keeping the first successful result. `a | b` fails only if both operands fail: dual to conjunction, their values union into a `copack`, and their errors combine into a `pack`. If both operands share the same value type, the value side remains ungraded. A `void` operand enters the sum as `pack<>`.
Disjunction evaluates alternative computations, keeping the first successful result. `a | b` fails only if both operands fail: dual to conjunction, their values union into a `copack`, and their errors combine into a `pack`. If both operands share the same value type, the value side remains ungraded, so two `void` operands stay `void`. Otherwise a `void` operand enters the sum as `pack<>`.

If either error side is graded, the product distributes over it: $(E_1 + E_2) \times F \to (E_1 \times F) + (E_2 \times F)$ (the full Cartesian product when both are graded), yielding a canonical `copack` of `pack`s.

Expand Down Expand Up @@ -590,7 +595,7 @@ Because member `.and_then` cannot change carrier families (Section 3), its *Klei
- Copack-graded `expected` unions heterogeneous error sets.
- Copack-valued inputs join heterogeneous successful branch types into a normalized `copack`.
- Branch convergence preserves the exact type without duplicate union states.
- All-`void` branches join to `void`; mixed void and non-void branches are ill-formed.
- All-`void` branches join to `void`; in a mix of `void` and non-`void` branches, a `void` one joins as `pack<>`.
- Callback results returning bare values require `transform` rather than `and_then`.

The library formalizes this "same-kind" contract via the `fn::same_kind` concept, which lets generic templates probe whether two carrier types belong to the same monadic family:
Expand Down Expand Up @@ -648,14 +653,14 @@ Value joining and error grading are independent: branch values can join while th
During sequential composition, `libfn` derives the promoted type from the `copack` you supply:

- In `and_then` (success binding), a plain error type `E` is promoted to `copack<E>` if the returning **error type** of the callback is `copack<E>`.
- In `or_else` (recovery/error binding), a plain success type `T` is promoted to `copack<T>` if the returning **success type** of the callback is `copack<T>`.
- In `or_else` (recovery/error binding), a plain success type `T` is promoted to `copack<T>` if the returning **success type** of the callback is `copack<T>`; for `void` that lift is `copack<pack<>>`.

An un-graded computation thus enters a graded pipeline without manual lifting.

If you need to perform this promotion explicitly on the carrier itself before entering a composition, `libfn` provides direct member helpers:

- `.copack_error()` on `expected` explicitly lifts the error, transforming `expected<T, E>` to `expected<T, copack<E>>`.
- `.copack_value()` on `expected` explicitly lifts the success value, transforming `expected<T, E>` to `expected<copack<T>, E>`.
- `.copack_value()` on `expected` explicitly lifts the success value, transforming `expected<T, E>` to `expected<copack<T>, E>`, and `expected<void, E>` to `expected<copack<pack<>>, E>`.
- `.copack_value()` on `optional` symmetrically lifts the value, transforming `optional<T>` to `optional<copack<T>>`.

These helper methods provide a compact, explicit alternative to the pipeline promotions:
Expand Down Expand Up @@ -1113,7 +1118,7 @@ Index:
* The term **identity cluster** is introduced in Section 3 under the infallible carriers as a simple grouping definition: "_Together, `just`, `choice` and `expected<T, copack<>>` form the identity cluster_." It does not expand on its operations or mathematical properties here, keeping the introduction minimal.

**2. Core Algebraic Foundations (Sections 2, 3, & 4)**
* **Section 2** establishes the core algebraic identities of types (**0**, **1**, **A + B**, **A × B**), separates Zero (`copack<>`) from Unit (`pack<>`), and defines `copack` set semantics (deduplication, flattening, sorting) and why the algebra is strictly opt-in.
* **Section 2** establishes the core algebraic identities of types (**0**, **1**, **A + B**, **A × B**), separates Zero (`copack<>`) from Unit (`pack<>`), and defines `copack` set semantics (deduplication, flattening, sorting), why the algebra is strictly opt-in, and how `void` enters it.
* **Section 3** establishes the carriers, the fact that raw data lacks control flow while carriers have it, and the basics of cross-carrier recovery-path bridging.
* **Section 4** expands on the concrete C++ payload behaviors of `pack` and `copack` (ADL `get`, structured bindings, `.append()`, singular copacks).

Expand Down
27 changes: 10 additions & 17 deletions docs/reference/conjoin.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,38 +42,31 @@ template <some_expected Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (3)

template <typename Lh, some_expected_void Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected<typename std::remove_cvref_t<Lh>::value_type, typename std::remove_cvref_t<Rh>::error_type>; // (4)
constexpr auto operator&(Lh &&, Rh &&rh); // (4)

template <some_expected_void Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected<typename std::remove_cvref_t<Rh>::value_type, typename std::remove_cvref_t<Lh>::error_type>; // (5)

template <typename Lh, some_expected Rh>
constexpr auto operator&(Lh &&, Rh &&rh); // (6)

template <some_expected Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&); // (7)
constexpr auto operator&(Lh &&lh, Rh &&); // (5)

template <typename Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (8)
constexpr auto operator&(Lh &&, Rh &&rh) -> std::remove_cvref_t<Rh>; // (9)
constexpr auto operator&(Lh &&lh, Rh &&) -> std::remove_cvref_t<Lh>; // (10)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (6)
constexpr auto operator&(Lh &&, Rh &&) -> just<void>; // (7)

template <some_optional Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (11)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (8)

template <typename Lh, some_optional Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (12)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (9)

template <some_optional Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&rh); // (13)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (10)

template <typename Lh, some_optional Rh>
constexpr auto operator&(Lh &&, Rh &&rh); // (14)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (11)

template <some_optional Lh, typename Rh>
constexpr auto operator&(Lh &&lh, Rh &&); // (15)
constexpr auto operator&(Lh &&lh, Rh &&rh); // (12)

constexpr auto operator&(auto &&lh, auto &&rh); // (16)
constexpr auto operator&(auto &&lh, auto &&rh); // (13)
```

:include-doxygen-doc: fn::operator& { args: "Lh &&, Rh &&" }
Expand Down
3 changes: 1 addition & 2 deletions examples/type_algebra/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,8 @@ auto test_conjunction_with_identity_cluster(fn::expected<int, Error> ex, fn::jus
auto res1 = ex & j;
static_assert(std::same_as<decltype(res1), fn::expected<fn::pack<int, double>, Error>>);

// Conjoining with a unit (just<void>) completely elides the unit
auto res2 = ex & fn::just<void>{};
static_assert(std::same_as<decltype(res2), decltype(ex)>);
static_assert(std::same_as<decltype(res2), fn::expected<fn::pack<int>, Error>>);

// Conjoining a choice causes distribution inside the carrier
fn::choice<bool, double> ch = 1.5;
Expand Down
Loading
Loading