diff --git a/.gitignore b/.gitignore index a5b79220..13d3ad82 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ build/ .scratch*/ dist*/ result* -__pycache__/** +__pycache__/ .cache/ .devcontainer/ .venv/ @@ -23,3 +23,4 @@ CMakeUserPresets.json bazel-* MODULE.bazel.lock .bazelversion +.memsearch/** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98d92c38..e2b6d16d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,9 +61,16 @@ repos: language: python files: ^include/.*\.hpp$ pass_filenames: false + # NOTE each document and its example must match a DOCUMENTS entry in scripts/sync_md_examples.py - id: sync-readme-example - name: sync README example with examples/readme - entry: python scripts/sync_readme_example.py + name: sync README code fences with examples/readme + entry: python scripts/sync_md_examples.py README.md language: python files: ^(README\.md|examples/readme/main\.cpp)$ pass_filenames: false + - id: sync-type-algebra-examples + name: sync TYPE_ALGEBRA code fences with examples/type_algebra + entry: python scripts/sync_md_examples.py TYPE_ALGEBRA.md + language: python + files: ^(TYPE_ALGEBRA\.md|examples/type_algebra/main\.cpp)$ + pass_filenames: false diff --git a/CLAUDE.md b/CLAUDE.md index e93ec2e2..6ff7df61 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,68 +2,82 @@ Conventions for AI agents in this repo (you are the primary reader — keep this terse). +The project is libfn: a header-only C++20 functional-programming library — `fn` (monadic +composition and types) layered over `pfn` (polyfills of C++23/26 vocabulary types). + ## Collaboration - Pushing back **and** asking questions is welcome — a challenged design beats a silently implemented flawed one. ## CI -- Red CI is top priority — fix before other work; a failed build masks failures behind it. Check CI state when starting new work: via `gh` if available, else ask the user. -- `gh` is **optional** — use it when `GH_TOKEN` is set (repo `libfn/functional`); scope varies per session, so attempt what the user asks and fall back to drafting/asking when a permission blocks. The token is a RAM-only, short-lived PAT injected at launch — never `gh auth login` (persists it to disk). +- Red CI is top priority — fix before other work; a failed build masks failures behind it. At session start (top-level agent, not subagents): check CI state via `gh` if available, else ask the user. +- Before editing `.github/workflows/`, read CONTRIBUTING `## GitHub Actions workflow pitfalls`. +- `gh` works iff `GH_TOKEN` is set; its scope varies per session — on a permission block, fall back to drafting/asking. Never `gh auth login` (would persist the ephemeral token to disk). `gh pr edit` / `gh issue view` SILENTLY no-op under this token — use `gh api` instead. -## Commits +## Commits & GitHub text -- Trailer `Assisted-by: Claude:` (Linux-kernel convention), e.g. `claude-opus-4-8`. No `Co-Authored-By:`. -- Offer commits; never commit without confirmation. Terse messages: imperative topic, body only if needed. +- Trailer `Assisted-by: Claude:` (Linux-kernel convention), e.g. `claude-opus-4-8`. This replaces the harness's trailer boilerplate entirely — no `Co-Authored-By:`, no `Claude-Session:` URL. Same rule for GitHub issues, PRs and comments: `Assisted-by:` is welcome; no other footers or attribution boilerplate. +- Offer commits; never commit without the user's confirmation — which may be relayed to a commit subagent by the parent that received it. Terse messages: imperative topic; body only when the change needs a *why* (the routing rule in Code names that case). +- If a pre-commit hook rewrites staged files, the commit aborts — re-stage the same files and retry once (CONTRIBUTING `## Pre-commit`). - A feature or fix commit should include a test for the behaviour it changes; exceptions are allowed. The PR must contain such a test somewhere unless the behaviour is inherently untestable (for example, because of language or compiler limitations); explain the omission. - Never `git push` or sign commits — the user signs (GPG) and pushes. ## Git state -- Starting work, orient first: `git status -sb` + `git log --oneline -5` — catches silent branch switches; unpushed commits await the user's push. Read-only git is free; `git diff` can be large — use judiciously. +- At session start (top-level agent, not subagents), orient first: `git status -sb` + `git log --oneline -5` — catches silent branch switches; unpushed commits await the user's push. `git diff` can be large — use judiciously. + +## Build & verification + +- Build/test is CMake + Catch2, one ctest target per test source; toolchain, options and modes: CONTRIBUTING `## Building locally`. Local build trees are gitignored siblings named `.build*` — reuse an existing tree rather than configuring a fresh one per task. +- Watch every gate's output: never send a build or test run to `/dev/null`, and read the failing tail as well as the exit code. Rebuild before rerunning tests — a stale binary passes the tests it was built from. ## Code - Default to no comment; assume the reader reads the surrounding code. Comment only where the *why* stays non-obvious despite that context (constraint/invariant/workaround/surprise); never restate code; no boilerplate docstrings. - Routing: *unusual code* → comment; *ordinary code, noteworthy change* → commit body; *both obvious* → neither. "Context" = code the reader sees; why-the-change → commit. -- Don't create `.md`/summary/planning files unless asked. +- Don't create `.md`/summary/planning files unless asked (memory files are exempt — see Memory). - A new file's copyright year = the year it enters the codebase (the current year; if unsure, infer from the latest commit). - In `include/` headers, anchor the standard library as `::std::`, never bare `std::` — a user's `fn::std` would otherwise win lookup inside namespace `fn`. Not needed in tests. ## Layering -Four header layers; each may depend only on those below it: -- `include/fn` — may use `fn/detail` and `pfn` -- `include/fn/detail` — may use `pfn`, never `fn` -- `include/pfn` — C++23/26 polyfill; standalone except for the version header -- `include/libfn_version.hpp` — base: the sole root header, no dependencies +- Four header trees under `include/`, strictly layered — `fn` → `fn/detail` → `pfn` → `libfn_version.hpp`; each may depend only on layers after it, never back. Rules and the hoist technique (making an `fn` facility available to `fn/detail`): CONTRIBUTING `## Header layering`. +- Inline-namespace versioning wrap: CONTRIBUTING `## Versioning`; pre-commit enforces. -Every `namespace fn` opening in `include/` carries `inline namespace LIBFN_VERSION`, every `namespace pfn` opening `inline namespace LIBFN_VERSION_BASE` — the mode-less spelling; pfn never joins the `_cxx26` ABI twin (pre-commit enforced). A header that opens either includes `` itself. +## C++ standard versions -To give an `fn/detail` file something that lives in `fn`, hoist it: the implementation moves into `fn/detail/X.hpp` as `fn::detail::_name` (no doxygen — detail headers aren't user-facing); `fn/X.hpp` stays a thin public wrapper re-exporting it as `fn::name` (pattern: `fn/functional.hpp`). +- C++20 is the baseline: `include/` relies on C++20 only in every default-mode build, and `pfn` never relies on post-C++20 features in any mode. Spell C++23-isms as C++20: `static operator()` → `const` member; a `static constexpr` local in a constexpr function → non-static; `std::unreachable` → `pfn::unreachable`; `0uz` → `std::size_t{0}`. +- `LIBFN_CXX26` (strict opt-in): C++26 reliance — `std::type_order` behind its capability gate; details in CONTRIBUTING `### Standard-mode feature reliance`. -## C++20 +## Client code -C++20 is the sole export surface — fn + pfn build and pass tests as C++20 on all supported compilers, incl. MSVC; CI validates C++23 via the test-only `VALIDATE_CXX23` lanes. Keep `include/` C++20 — spell C++23-isms as C++20: `static operator()` → `const` member; a `static constexpr` local in a constexpr function → non-static; `std::unreachable` → `pfn::unreachable`; `0uz` → `std::size_t{0}`. +- Code written against the library — examples, documentation snippets, reproducers, anything a user of libfn might imitate: follow CONTRIBUTING `## Client code`. ## Tests - Before writing or reviewing tests, read and follow `CONTRIBUTING.md` from `## Unit tests` to the next top-level heading; it is the source of truth for test structure, assertions and compile-time probes. +## Delegation + +- Delegate mechanical, well-specified, verifiable work — builds, test sweeps, commit mechanics, compile probes — to subagents by default; their noise stays out of the working context. Run independent delegations concurrently. +- Keep exploratory reading, design and diagnosis inline: a subagent returns its result, not the understanding built producing it, and for this work the understanding is the point. +- Local agent definitions may exist under `.claude/agents/` (not committed); prefer them when present. Agents register at session start — a new or edited definition is invisible until the session restarts. +- Verify a finding empirically before filing it anywhere; for a compile-time claim that means a probe compiled on both gcc and clang. + ## Tooling -- Prefer `clangd-lsp@claude-plugins-official` over grep/whole-file reads for C++ symbol navigation (go-to-def, find-refs) and post-edit diagnostics — targeted lookups should cut context, not add it. Needs a populated `compile_commands.json`; if unavailable/empty, ask the user to populate it and offer help. +- When the `clangd-lsp@claude-plugins-official` plugin is available, prefer it over grep/whole-file reads for C++ symbol navigation (go-to-def, find-refs) and post-edit diagnostics — targeted lookups should cut context, not add it. Needs a populated `compile_commands.json`; if unavailable/empty, ask the user to populate it and offer help. - clangd reflects one local toolchain, not the CI matrix — a clean clangd buffer is NOT portability clearance; full `-Werror` builds + CI stay the authority. ## Memory -- Keep memory current as facts change. -- Create memory files without asking, but announce each one and its purpose. -- On wrap-up or a "memory pass" request: review memory — update/remove obsolete, flag new. +- On wrap-up or a "memory pass" request: curate memory — update or remove obsolete entries, capture new durable facts; announce each new memory file and its purpose. A "consolidation pass" request does the same against recent session journals. ## Docs -- Map: README.md = user-facing overview (purpose, usage, project shape, support surface; no agent directives, no internal mechanics; CI surfaced as evidence only, never mechanics); CONTRIBUTING.md = contributor facts (coding + tests standards, build environment, workflows, all CI details, mechanics of every aspect; no agent directives, no library usage); CHANGELOG.md = design history (dated entries, newest first); docs/ = API reference (Doxygen → Pages; also usage); CLAUDE.md = agent practice + the critical selection of standards (coding, tests, documentation). -- Living documents (README, CONTRIBUTING, docs/) are timeless present tense — no "now", "no longer", "previously"; when reading or updating them, remove recency bias. A change that obsoletes documented design gets a dated CHANGELOG.md entry in the same change, saying what it obsoleted and why. -- Recency bias defence (all documentation except CHANGELOG.md, and code comments): you over-weight whatever you just worked on, so text written right after a change reads as a diff against your context, not a document for a reader who arrives fresh. The banned transition words are only the shallow symptom; test deeper: (1) day-one test — would this sentence exist had the feature or fix always been here? if not, cut it; (2) effort test — is the detail sized by reader need, or by how hard the work was? cut whatever answers questions no reader asked; (3) placement test — is it where a newcomer would look, or where your recent work pulls it? Defence: after editing, reread the whole file top-to-bottom as a first-time reader and re-judge the new text's length and position against the whole document — never review only your diff. CHANGELOG.md and commit messages are exempt: both are read as an increment on top of previous state, so change-perspective is their correct form. +- Map: README.md = user-facing overview (purpose, usage, project shape, support surface; no agent directives, no internal mechanics; CI surfaced as evidence only, never mechanics); CONTRIBUTING.md = contributor facts (coding + tests standards, build environment, workflows, all CI details, mechanics of every aspect; no agent directives, no library usage); TYPE_ALGEBRA.md = the design document — the library's type algebra worked from first principles; CHANGELOG.md = design history (dated entries, newest first); docs/ = API reference (Doxygen + znai → Pages; also usage); CLAUDE.md = agent practice + guardrails pointing into the above. +- Fenced C++ examples in README.md and TYPE_ALGEBRA.md are generated from sources in `examples/` by `scripts/sync_md_examples.py` (pre-commit keeps them in sync) — edit the example source, never the fence; prose edits stay outside fences. +- Living documents (README, CONTRIBUTING, docs/) are timeless present tense — no "now", "no longer", "previously". A change that obsoletes documented design gets a dated CHANGELOG.md entry in the same change, saying what it obsoleted and why. +- Recency-bias and wordiness defence (all human-readable text — docs, code comments, and the like; CHANGELOG.md and commit messages are exempt — change-perspective is their correct form): before keeping new text, test — (1) day-one: would this sentence exist had the feature or fix always been here? (2) effort: is detail sized by reader need, or by how hard the work was? — a hard-won bugfix earns no extra words; its history lives in `git log`/`git blame`/CHANGELOG.md; (3) placement: is it where a newcomer looks, or where your recent work pulls it? (4) economy: could fewer words say it as well? After editing, delegate a whole-file top-to-bottom reread to a subagent briefed as a first-time reader, blind to what changed — never review only your diff. Triage its findings: fix what your edit touches, surface the rest rather than rewriting unasked. - On memory or practice changes, check the root `.md` files for drift from reality and **offer** fixes (CLAUDE.md = practice, README/CONTRIBUTING = facts). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e2417e3..5bd43a48 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,29 @@ Match the assertion to the kind of fact being tested. Choose fixtures with care when probing exception specifications. `helper_t` has a separate constructor for each value category, and its non-const-lvalue copy constructor is always `noexcept`. A `helper_t` configured to throw is therefore not throwing for every value category, and a specification that remains `noexcept` for that constructor may be correct. Use a plain local type when the test needs every relevant construction to be potentially throwing. +## Client code + +Code written against the library — examples, documentation snippets, reproducers, anything a user of libfn might imitate: + +* Let the library derive graded error types. Never spell a multi-alternative `copack<...>` by hand — the canonical alternative order is internal and platform-dependent (MSVC orders class types after fundamentals); use `copack_for<...>`, and pin a deduced type with `static_assert` where the type is the point. +* Give behaviour a constant-evaluation twin: a `static_assert` replaying the same operations. UB is ill-formed in a constant expression, so the compiler diagnoses what a runtime run may silently get wrong — users rely on the library in constexpr for exactly this. +* Prefer monadic composition to unchecked access. `value()` is the library's only throw; reaching for it where `and_then`/`transform` would carry the value teaches the wrong idiom. +* Don't trust a bare `requires`-probe of a libfn call: several refusals (`apply` on mismatched branches, `and_then` on grade mismatches) are `static_assert`s inside the callee, so the probe answers true and instantiation hard-errors. Compile the call to know; negative viability probes must be dependent (see `### Compile-time probes` above). +* Outside `LIBFN_CXX26`, the internal type ordering is not expected to work with unnamed types or types without linkage — no lambdas or local types as `copack` alternatives in portable code. +* One libfn version per binary: header-only makes mixing versions an ODR violation, including two `z` releases of the same `y` line; default and `LIBFN_CXX26` builds never link as one, by design — see README `## Versioning and ABI`. +* A standalone reproducer (compiler or library bug) proves nothing until it is UB-free: gate it on UBSan and ASan with empty stderr as the criterion, not the exit code. + +## Header layering + +Four header trees under `include/`, each depending only on those below it: + +* `fn` — may use `fn/detail` and `pfn` +* `fn/detail` — may use `pfn`, never `fn` +* `pfn` — the C++23/26 polyfill; standalone except for the version header +* `libfn_version.hpp` — the sole root header, no dependencies + +To make an `fn`-level facility available to `fn/detail`, hoist it: the implementation moves into `fn/detail/X.hpp` as `fn::detail::_name` (detail headers are not user-facing and carry no Doxygen); `fn/X.hpp` remains a thin public wrapper re-exporting it as `fn::name` (pattern: `fn/functional.hpp`). + ## Versioning `VERSION` (in the repository root) is the single source of truth for the project version. A pre-commit hook (`scripts/sync_versions.py`) mirrors it into `ports/libfn/vcpkg.json` (`version-semver`), `MODULE.bazel`, and `include/libfn_version.hpp` — the header defining the `LIBFN_VERSION` macro that names the ABI-versioning inline namespace wrapping `fn`, and its mode-less sibling `LIBFN_VERSION_BASE` wrapping `pfn` (the layer rule below). Do **not** hand-edit those version literals — edit `VERSION` and let the hook sync them. @@ -75,7 +98,7 @@ This repository uses [pre-commit](https://pre-commit.com/) to enforce formatting python3 -m venv .venv source .venv/bin/activate pip install -r ci/pre-commit/requirements.txt -# Now install the pre-commit hooks locally +# Install the pre-commit hooks locally pre-commit install ``` diff --git a/README.md b/README.md index 4fc9afd5..d95a8f66 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# functional +# libfn -Functional programming in C++ +**Functional programming in C++** [![codecov](https://codecov.io/gh/libfn/functional/graph/badge.svg?token=3RHT38SEU0)](https://codecov.io/gh/libfn/functional) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flibfn%2Ffunctional.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Flibfn%2Ffunctional?ref=badge_shield) @@ -12,6 +12,7 @@ The purpose of this library is to exercise an approach to functional programming ## Example + ```cpp // Various error types. enum class NotANumber {}; @@ -33,9 +34,9 @@ class Rational { constexpr Rational(int n, int d) noexcept : n_(n), d_(d) {} public: - constexpr bool operator==(Rational const &) const noexcept = default; - constexpr int num() const noexcept { return n_; } - constexpr int den() const noexcept { return d_; } + constexpr auto operator==(Rational const &) const noexcept -> bool = default; + constexpr auto num() const noexcept -> int { return n_; } + constexpr auto den() const noexcept -> int { return d_; } // The invariants live in the type: `make` is the only way to build a `Rational`, and every one is // reduced, sign-normalized and representable. Callers receive a value they never need re-check. @@ -58,28 +59,28 @@ public: return Rational(static_cast(n), static_cast(d)); } - constexpr auto operator()(std::string_view s) const noexcept + constexpr auto operator()(std::string_view s) const noexcept -> decltype(auto) { return parse(s) | fn::and_then(*this); } } make{}; - constexpr auto neg() const noexcept { return make(-1LL * n_, d_); } - constexpr auto inv() const noexcept { return make(d_, n_); } - constexpr auto add(Rational const &other) const noexcept + constexpr auto neg() const noexcept -> decltype(auto) { return make(-1LL * n_, d_); } + constexpr auto inv() const noexcept -> decltype(auto) { return make(d_, n_); } + constexpr auto add(Rational const &other) const noexcept -> decltype(auto) { return make(1LL * n_ * other.d_ + 1LL * other.n_ * d_, // 1LL * d_ * other.d_); } - constexpr auto sub(Rational const &other) const noexcept + constexpr auto sub(Rational const &other) const noexcept -> decltype(auto) { return other.neg() | fn::and_then([*this](Rational y) { return add(y); }); } - constexpr auto mul(Rational const &other) const noexcept + constexpr auto mul(Rational const &other) const noexcept -> decltype(auto) { return make(1LL * n_ * other.n_, 1LL * d_ * other.d_); } - constexpr auto div(Rational const &other) const noexcept + constexpr auto div(Rational const &other) const noexcept -> decltype(auto) { return other.inv() | fn::and_then([*this](Rational y) { return mul(y); }); } @@ -88,7 +89,7 @@ public: // `evaluate` parses each operand, applies the operator, and lets `make` re-check the result. // Each stage fails its own way, and the library folds error types into one co-product. constexpr auto evaluate(std::string_view a, fn::copack_for op, - std::string_view b) noexcept + std::string_view b) noexcept -> decltype(auto) { using Op = fn::expected>; return (Rational::make(a) & Op{op} & Rational::make(b)) // @@ -114,19 +115,19 @@ static_assert(evaluate("2/3", Div{}, "0/1").error().has_value()); The library features demonstrated by the code example above: -* **Monadic sequences** — `operator|` pipes a `fn::expected` (or `fn::optional`) through operations: `and_then` and `transform` act on the value, `or_else`, `recover` and `transform_error` on the error, with `filter`, `inspect`, `fail` and more besides. -* **Graded errors** — each stage fails its own way — a malformed string, a zero denominator, an out-of-range result — and the library folds these into one `fn::copack` whose type it derives for you: here `fn::copack`, never spelled by hand. -* **Composing values** — `operator&` gathers successful operands left to right: two values become a `fn::pack`, a third appends to it. A `fn::pack` is a heterogeneous product — the operands as one value, spread into the next call; for example in `make`, where a `pack` returned from `parse` is passed to an overload taking two numbers. -* **Composing alternatives** — when a side is a `fn::copack` (a co-product — one of several types, indexed by type, not by position like `std::variant`), `&` distributes over it, pairing every alternative with the other operand. Two copacks yield the full cartesian product. The result type is flattened, deduplicated and sorted for you. +* **Monadic sequences** — `operator|` pipes a `expected` (or `optional`) through operations: `and_then` and `transform` act on the value, `or_else`, `recover` and `transform_error` on the error, with `filter`, `inspect`, `fail` and more besides. +* **Graded errors** — each stage fails its own way — a malformed string, a zero denominator, an out-of-range result — and the library folds these into one `copack` whose type it derives for you: here `copack`, never spelled by hand. +* **Composing values** — `operator&` gathers successful operands left to right: two values become a `pack`, a third appends to it. A `pack` is a heterogeneous product — the operands as one value, spread into the next call; for example in `make`, where a `pack` returned from `parse` is passed to an overload taking two numbers. +* **Composing alternatives** — when a side is a `copack` (a co-product — one of several types, indexed by type, not by position like `std::variant`), `&` distributes over it, pairing every alternative with the other operand. Two copacks yield the full cartesian product. The result type is flattened, deduplicated and sorted for you. * **Multidispatch** — the pack (or copack of packs) flows into the next stage as separate arguments. An `fn::overload` — or any function — dispatches on the runtime alternative by ordinary overload resolution. Dispatch is exhaustive: a missing handler is a compile error. -* **Identity monad** — `fn::expected>` cannot hold an error (enforced at compile time), a spelling of the identity monad; the example lifts `op` into it as `Op`. +* **Identity monad** — `expected>` cannot hold an error (enforced at compile time), a spelling of the identity monad; the example lifts `op` into it as `Op`. * **No surprises** — libfn throws no exceptions of its own (only `value()`, as the standard mandates), and composes safely with callables that do; it allocates no memory of its own and performs no I/O. Being fully `constexpr`, it can drive a program evaluated entirely at compile time, where the compiler diagnoses any undefined behaviour. -The example also demonstrates how well libfn works with general programming idioms. `make` is a *smart constructor* — the only way to build a `Rational` — enforcing the type's invariants and returning `fn::expected`: callers never need to re-check what the type guarantees. Treating *callables as values* lets operations such as `and_then` accept `make` whole, carrying its overload set. +The example also demonstrates how well libfn works with general programming idioms. `make` is a *smart constructor* — the only way to build a `Rational` — enforcing the type's invariants and returning `expected`: callers never need to re-check what the type guarantees. Treating *callables as values* lets operations such as `and_then` accept `make` whole, carrying its overload set. These properties also make libfn a natural fit for asynchronous composition, such as coroutines or senders/receivers. Operations and monadic types alike are plain values: `and_then(f)` is a *description* of a step, executed only when a monad is piped into it (an input to the sequence, or the result of the preceding operation). A framework can hold the steps of a computation and apply them as results arrive, with a strongly typed error channel and no hidden control flow — exactly what such programming models need. -Beyond the example: `fn::choice` (a monad over `fn::copack`); the same operations over `fn::optional` as over `fn::expected`; tuple protocol in `fn::pack` (`get(p)` or structured bindings); `fn::pack` and `fn::copack` are both structural types (a `constexpr` value which may be used as a template parameter); support for immovable values and callables; and more — see [examples/](examples/) and the [API reference][docs]. +Beyond the example: `choice` (a monad over `copack`); the same operations over `optional` as over `expected`; simultaneous disjunction (using `operator|` to fallback-combine monadic computations) and its `fn::disjoin` fold; `fn::conjoin` for simultaneous product folds; tuple protocol in `pack` (`get(p)` or structured bindings); `pack` and `copack` are both structural types (a `constexpr` value which may be used as a template parameter); support for immovable values and callables; an extensible pipeline, where a verb defined outside the library pipes exactly like the built-in ones; and more — see [examples/](examples/) and the [API reference][docs]. ## How @@ -181,6 +182,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the development environment, building ## License +Distributed under the ISC License; see [LICENSE.md](LICENSE.md) for the terms. + [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flibfn%2Ffunctional.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Flibfn%2Ffunctional?ref=badge_large) diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md new file mode 100644 index 00000000..43607af2 --- /dev/null +++ b/TYPE_ALGEBRA.md @@ -0,0 +1,1079 @@ +# Type algebra and functional composition in libfn + +A C++20 functional programming library, `libfn` lets the compiler derive the static shape of a computation alongside its values. Rather than collapsing failure into one wide error type, `libfn` tracks the precise algebraic combinations of success, alternative, and error states as operations are chained into expressions. + +The library operates on two payload types and four computation carriers: + +- `pack`: Product type containing all fields; a tuple-like data structure. +- `copack`: Canonical coproduct containing exactly one alternative; a variant-like disjoint set of types. +- `optional`: Computation yielding a value or empty. +- `expected`: Computation yielding success or error. +- `choice`: Never-failing computation holding one of several alternatives. +- `just`: Never-failing computation yielding a single value or a `void`. + +Composition operations include `transform` (mapping), `transform_error` (error mapping), `and_then` (sequential monadic binding), `or_else` (recovery), `operator&` (conjunction / simultaneous product composition), `operator|` (disjunction / simultaneous sum composition), and the n-ary folds `fn::conjoin` and `fn::disjoin`. Elimination is `apply` (multidispatch). + +### Member vs. Pipeline Syntax + +Some operations are exposed in two forms: + +- **Member functions** (e.g., `.transform()`, `.and_then()`, `.apply()`) called directly on a carrier or payload (e.g., `ex.transform(f)`, `cp.apply(f)`). +- **Pipeline functors** in namespace `fn` (e.g., `fn::transform`, `fn::and_then`) applied via `operator|` (e.g., `ex | fn::transform(f)`). + +There are also pipeline functors (`recover`, `fail`, `filter`, `inspect`, `inspect_error`, `discard`) which have no member spelling, and member functions (`apply`, `apply_r`, `apply_type`) which have no pipeline spelling. + +The `operator|` carries two meanings, told apart by its right operand: a pipeline functor on the right feeds the carrier into that operation, while another carrier on the right is disjunction (explained in Section 7). + +Freestanding `fn::apply(f, args...)` is the general multidispatch entry point: it accepts any mix of scalars, tuple-like structures, `pack`s and `copack`s, unpacking products and dispatching over alternatives in a single call. Do not confuse with `pfn::apply`, which is a polyfill for the C++26 `std::apply`, meant for C++20 compilers. The `fn::apply` is an extension on top of `pfn::apply`. + +In prose, we omit prefixes (writing `apply`, `transform`, `and_then`, `expected`, `pack`) when referring to both forms or core vocabulary types generally. + +### Storage Shape vs. Call Shape + +Although different types can behave identically during application, they remain strictly distinct in memory. For example, `pack`, `std::tuple`, and `std::pair` all unpack into the same call shape `f(a, b)` during `apply`, but they are separate C++ types with distinct layouts. Application does not silently convert or unify types on the storage side. + +To illustrate these concepts, the examples in this document use a reusable set of value and error types: + + +```cpp +struct UserId {}; +struct User {}; +struct FilePath {}; +struct MaximumSize {}; +struct BlockSize {}; + +struct NotANumber {}; +struct OutOfRange {}; +struct Missing { + auto operator<=>(Missing const &) const = default; +}; +struct IoError {}; +struct BadSyntax {}; +struct UnknownKey {}; +``` + +## 1. Why compose types as well as values? + +In idiomatic C++, error handling usually means picking one application-wide error enumeration, a giant `std::variant`, or throwing exceptions. If a function only ever fails due to one specific error, returning a large application-wide error variant discards the precise bounds of what the function can actually do. + +With `libfn`, the compiler derives an exact, graded error pipeline. Consider parsing, validating, and loading a user: + + +```cpp +auto parse_id(std::string_view) -> fn::expected>; +auto validate(UserId) -> fn::expected>; +auto load(UserId) -> fn::expected>; + +auto graded_pipeline(std::string_view sv) -> void +{ + auto pipeline = parse_id(sv) | fn::and_then(validate) | fn::and_then(load); + + // The exact derived error union is recorded in the type: + static_assert( + std::same_as>>); +} +``` + +The resulting `expected` statically records that the pipeline yields a `User` on success, or fails with exactly one of `NotANumber`, `OutOfRange`, `Missing`, or `IoError`. This exact union accumulates automatically via `and_then` composition. + +### What Does "Graded" Mean? + +Standard monads are rigid: an `expected` requires every step in a pipeline to return the identical error type `E`. This forces you to define a monolithic global error union upfront. + +A **graded monad** relaxes this restriction. Each operation is indexed by a "grade"—a set representing all its specific possible errors (its "effects") by means of `copack`, which is a disjoint set of types. As you chain operations, the compiler automatically adds these grades to the set. +The resulting error type is **graded**: it expands (or narrows during recovery) to match the *exact* subset of errors possible in the compiled path, providing strict static effect tracking (subeffecting) with zero boilerplate. + +You may also use `copack` on a value side of most carriers (except for `just>`, which must be spelled `choice`). Grading is opt-in for chaining: a `copack` on the error side enrols an `expected` in this union arithmetic, while a plain `expected` holds every step to the identical error type `E`. A `copack` on the value side enrols an `expected` or `optional` into the same arithmetic on values. + +> [!TIP] +> +> ### Mathematical note — graded monads +> +> Formally, a graded monad (also known as an effect monad) indexes a family of monadic carriers over a partially ordered monoid (pomonoid) of effects $(\mathcal{E}, \bullet, I, \le)$. +> +> In `libfn` that pomonoid is carried by the finite sets of C++ types: +> +> - **Grades ($\mathcal{E}$)**: Finite sets of alternative types (errors). +> - **Monoidal multiplication ($\bullet$)**: Set union ($\cup$), representing effect accumulation. +> - **Identity ($I$)**: The empty set ($\emptyset$), representing the zero-error/never-failing state. +> - **Partial order ($\le$)**: Subset relation ($\subseteq$), which licenses effect approximation (subeffecting / widening). +> +> Taking union as the multiplication makes this monoid commutative and idempotent — a join-semilattice — whose order is the one it induces, $E \le F \iff E \cup F = F$. Those are exactly the `copack` laws of Section 2, and they are why widening is coherent: grades accumulated in any order, through any intermediate supersets, land on the same normalized set. +> +> For a standard monad $M$, the binding operation maps $M\langle A\rangle \to (A \to M\langle B\rangle) \to M\langle B\rangle$. In `libfn`'s graded monad, *bind* accumulates effects across the pomonoid: +> +> $$bind : M_E\langle A\rangle \to (A \to M_F\langle B\rangle) \to M_{E \cup F}\langle B\rangle$$ +> +> Section 9 identifies the lax monoidal functor this determines. + +The same type precision extends to computations combined side by side rather than in sequence. Conjunction (`operator&`) evaluates independent computations and bundles them into a single carrier holding a `pack` of the successful values over a `copack` of the exact possible errors; disjunction (`operator|`) is its dual. Sections 6 and 7 cover both. + +### The Two Cooperating Mechanisms + +Behind these precise compiled types are two independent mechanisms that cooperate to derive and eliminate these shapes: + +1. **Type algebra** records and normalizes the exact stored C++ types using `pack` and `copack` as you compose operations. +2. **The application protocol** uses `apply` and ordinary C++ overload resolution to unpack those stored values and route them to your functions or lambdas. + +To route multiple alternative paths inside `apply`, the library provides `fn::overload`, which fuses unrelated lambdas into a single overload set. + +These derived types are the explanation of the library's design, not an internal template-metaprogramming implementation detail. + +## 2. Types as an algebra: zero, unit, alternatives, and products + +To derive strict programmatic shapes, `libfn` uses an algebraic vocabulary over types. + +- `copack<>` represents **0** (Zero) - an uninhabited type. +- `pack<>` represents **1** (Unit) - a type with exactly one state. +- `copack` represents **A + B** (Alternatives) - a coproduct where exactly one alternative is present. +- `pack` represents **A × B** (Products) - a type where all fields are present simultaneously. + +These states can also be used to express the standard vocabulary types: + +- `std::optional` ≅ **1 + T** (It is either empty/unit or contains `T`, similar to `copack_for`) +- `std::expected` ≅ **T + E** (It contains either success `T` or error `E`, similar to `copack_for>`) + +The symbol ≅ indicates an equivalent state shape (an information-level correspondence), not `std::same_as`. `std::optional` is its own distinct C++ type, but algebraically, it behaves as `1 + T`. + +### Zero is not unit + +In `libfn`'s algebra, zero and unit are strictly separated: + +- `copack<>` is uninhabited: you cannot construct it. Algebraically, it is `0`. +- `pack<>` is the one nullary product value. You can construct it via `pack<>{}` or `fn::as_pack()`. Algebraically, it is `1`. + +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 | +| ----------- | ------- | +| `expected>` | An expected value that **cannot fail** because its error state is uninhabited. | +| `expected, E>` | An expected value that **cannot succeed** because its success state is uninhabited. | +| `optional>` | An optional that **must be empty**, as its value state is uninhabited. | +| `expected` | An expected that yields **no value on success**, but can fail with `E`. | + +### Copacks use set semantics + +A major feature of `libfn` is that `copack` forms canonical sets of types, in contrast to the positional indexing of `std::variant`. When you combine types into a coproduct, `fn::copack_for` guarantees deduplication, flattening, and a canonical ordering. + + +```cpp +auto test_copack_set_semantics() -> void +{ + using SetA = fn::copack_for; + using SetB = fn::copack_for; + + // Flattening, deduplication, and reordering happen automatically: + using Union = fn::copack_for>; + + static_assert(std::same_as>); +} +``` + +> [!NOTE] +> +> ### Note — copack vs. copack_for +> +> In C++, there is no native language feature to represent a "set of types." Template parameter lists are always positional, variadic sequences. Syntactically, this means `copack` and `copack` would be completely distinct types—a property that directly violates the mathematical commutative law of set union. +> +> To enforce set semantics at compile time, `libfn` defines one canonical representation and rejects any instantiation that diverges from it: +> +> - **`copack`** is the core storage type. It requires its template parameters to already be flat, unique, and sorted in a strict total order over types. The order is derived from the compiler's own spelling of each type; a build targeting C++26 with `LIBFN_CXX26` set will use `std::type_order` to derive the order of types, while the default build uses a type sorting mechanism based on compiler-specific type names (since these two orders may differ, each defines a distinct ABI namespace). If you attempt to instantiate `copack` manually with out-of-order parameters (such as `copack` when `A` precedes `B` in that order) or with nested copacks (such as `copack>`), the compiler will reject the instantiation as outright ill-formed. +> +> - **`copack_for`** is the user-facing type alias. It accepts any list of types (out-of-order, duplicates, nested copacks), performs the flattening, deduplication, and canonical sorting, and resolves to the validated `copack`. +> +> In an API signature the two are the same type, since the alias resolves to `copack`; in prose and code, `copack` names the normalized *state shape* and `copack_for` the *construction utility*. `choice` and `choice_for` stand in the same relation, over the alternatives of the underlying `copack`. +> +> **Best practice**: spell `copack_for` / `choice_for` rather than `copack` / `choice`, so that no spelling in your project is tied to one compiler's ordering. + +The laws governing `copack` are: + +- **Commutative**: Order of types does not change the resulting set. +- **Associative**: Nesting copacks is equivalent to flattening them. +- **Idempotent**: Duplicate types are collapsed into one. +- **Identity**: `copack<>` acts as the union unit (adding `copack<>` changes nothing). + +Idempotence means a set cannot carry positional meaning: `copack_for` is one alternative, `copack`, not two. Types combined into a `copack` should therefore be strongly typed tag structs or distinct domain objects, never generic primitives whose meaning depends on where they sit. Two *distinct* types that the ordering mechanism cannot distinguish are a different matter: the library rejects them outright via a compile-time assertion rather than silently merging them, guaranteeing that no type is ever silently lost. + +### The algebra is strictly opt-in + +The library relies on explicit consent. It does not silently reinterpret arbitrary C++ types as products or coproducts: + +- `std::tuple` remains a standard tuple. +- `std::variant` does not acquire `copack` set semantics. +- Tuple-like participation in `apply` changes call shape applicability, but it does not change the stored type identity. +- A plain `expected` does not automatically become graded. + +To invoke the algebra, you use the opt-in mechanisms provided by the library: + +- Direct construction of `pack` and `copack_for`. +- Explicit conversions via `fn::as_pack` and `fn::as_copack`. +- Member helpers for explicit type lifting (detailed in Section 9). + +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. + +## 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 library defines exactly four carrier families, divided by their fallibility and payload capacity: + +### The fallible carriers + +- **`optional`** (representing $T + 1$): A carrier that either holds a successful value of type `T` or is empty (`std::nullopt`). +- **`expected`** (representing $T + E$): A carrier that either holds a successful value of type `T` or an error of type `E`. + - `expected` is infallible when its error side is `copack<>`. + +*(Note: `optional` and `expected` are the `fn` extensions of the standards-conforming `pfn` polyfills; Section 15 covers the two layers.)* + +### The infallible (identity) carriers + +- **`just`**: Always contains a single successful value of type `T`. +- **`choice`**: Always contains one of several selected alternatives, representing the complete state space of the computation. + +Because `choice` implies that an alternative is always present, `choice<>` is incomplete: an always-present selected alternative requires at least one alternative to exist. + +Additionally, the infallible state **`expected>`** (representing $T + 0 \cong T$) can never fail because `copack<>` represents the initial zero object **0** (the uninhabited type). Lacking any possible error alternatives, it acts as an infallible, graded unit context. Since it is a specialized state of `expected` rather than a unique template, it is classified under the same computation carrier. + +Together, `just`, `choice` and `expected>` form the **identity cluster**. + +> [!NOTE] +> +> ### Note — `just>` is spelled `choice` +> +> To carry several alternatives that cannot fail, use `choice`: a computation that always succeeds, with a result that is one of `Ts...`. Spelling the same shape as `just>` does not compile — `just` rejects a `copack` payload with `"a just over a copack is spelled choice"` — so the shape has one canonical spelling, exactly as `copack` has one canonical form for its alternatives. + +These carriers constrain their payloads. While `optional` is supported as a standard-conforming exception, other carriers reject raw reference types outright; references must be wrapped inside a `pack` (detailed in Section 4). + +### Carriers have control flow; raw data does not + +Raw algebraic constructs—such as `std::tuple` (product), `std::variant` (sum), or `libfn`'s own `pack` and `copack`—are passive data layouts. They contain no intrinsic control flow, no concept of short-circuiting, and no built-in notion of success versus failure. + +Composing computations requires wrapping these values in computation carriers. Product composition (conjunction) and sum composition (disjunction) operate on carriers, not raw data. The carrier manages success propagation and short-circuits failures. + +### Carrier Bridging: Interoperable Pipelines + +Because these carriers represent different computational contexts, pipelines often need to transition between them. `libfn` licenses explicit **cross-carrier bridging** via pipeline-scoped operations using `operator|`. + +Standard fallible carriers can bridge to each other on their error/empty recovery paths via pipeline functor `fn::or_else` (e.g., `expected` to `optional`, or vice versa). This is safe because on the success path, the successful value is preserved and bypasses the recovery callback. The transition only occurs on the handled failure branch, allowing you to gracefully convert a missing value into a concrete error, or decay a detailed error into an empty state: + + +```cpp +auto test_failure_bridge(fn::expected ex, fn::optional opt) -> void +{ + // Fallible carriers can bridge to each other on the failure/empty recovery path + auto expected_to_optional = ex | fn::or_else([](IoError) { return fn::optional{}; }); + static_assert(std::same_as>); + + auto optional_to_expected = opt | fn::or_else([]() { return fn::expected{100}; }); + static_assert(std::same_as>); +} +``` + +Member functions `.or_else` and `.and_then` (see Sections 8 and 10) do not support cross-carrier bridging to avoid coupling between different carriers. Pipeline functors, lying a layer above carriers, can perform these transitions without coupling. + +Identity carriers bridge in the other direction, on the success path; Section 10 covers that together with the identity cluster. + +## 4. The sum and product payloads: pack and copack + +Modeling complex algebraic structures—such as multi-field products or multi-alternative disjoint sums—requires specialized payload types. `libfn` provides two core vocabulary types: + +### pack: all fields are present + +A `pack` stores multiple fields and supports the standard C++ tuple protocol (`get`, `tuple_size`, `tuple_element`, structured bindings) and an `append` mechanism. Unlike standard tuples, `libfn` packs are strictly flat: a `pack` cannot be an element of another `pack`. Appending a `pack` splices its fields into the outer pack rather than nesting it. + +To explicitly lift values into a `pack` (which is useful when conjoining scalars with other packs or copacks), use `fn::as_pack(...)`. When called without template parameters, `as_pack` is deduction-only and preserves the value category of its arguments: `as_pack(42)` yields `pack`, whereas calling `as_pack(x)` on an lvalue `x` yields `pack` (a reference rather than a copy). + +Spelling the template parameters instead (e.g., `as_pack(x, d)`) takes deduction out of the picture: each argument is passed as the type you named, enabling implicit conversions to happen at the call boundary. A reference element becomes something you ask for explicitly — `as_pack(x)` yields `pack`. Note that partial template spelling is not supported; all element types must be spelled out explicitly if template parameters are specified. + + +```cpp +auto test_pack(int x = 12, double d = 3.14) -> void +{ + fn::pack p{UserId{}, User{}}; // CTAD + [[maybe_unused]] auto [id, user] = p; // Structured bindings work naturally + + // Ordered, non-deduplicated fields + using P = fn::pack; + static_assert(std::tuple_size_v

== 3); + + // Found via ADL (like std::get) + using std::get; + static_assert(std::same_as(p)), UserId &>); + + // Splicing scalars or other packs via append: + auto row = fn::pack{UserId{}}.append(FilePath{}); + auto wider = std::move(row).append(fn::pack{true, 3}); + + static_assert(std::same_as>); + + // Explicitly lifting a single scalar value into a pack: + auto lifted_lvalue = fn::as_pack(x); + static_assert(std::same_as>); + + auto lifted_rvalue = fn::as_pack(42); + static_assert(std::same_as>); + + // Spelling the element type explicitly can be used to opt out of reference preservation: + auto copied = fn::as_pack(x); + static_assert(std::same_as>); + // ... or to force a specific reference type (subject to parameter binding rules): + auto referenced = fn::as_pack(x); + static_assert(std::same_as>); + + // The explicit form also coerces - the argument converts at the call boundary: + auto coerced = fn::as_pack(x, d); + static_assert(std::same_as>); +} +``` + +### copack: one exact alternative is present + +As a payload, a `copack` models a discriminated union of types. + +Evaluating a `copack` via `.apply()` passes the active alternative to the callback. Because `copack` is self-flattening, nested `copack`s do not occur. A selected alternative that is itself tuple-like—such as `pack`, `std::tuple`, or `std::array`—is unpacked one level, passing its immediate constituents as separate arguments. Since normalized shapes are sums of products, one level of unpacking is sufficient to supply the product's fields as function arguments. + +To explicitly lift a single scalar value into a single-alternative coproduct, use `fn::as_copack(value)`. Unlike `as_pack`, it always decays: a `copack` alternative can never be a reference. When a `copack` contains exactly one alternative, it is **singular** and supports direct value extraction via the `get` utility (resolvable via ADL), which propagates references with the same semantics as `apply`. + +A `pack` can be lifted into a `copack` (including packs holding references), but a `pack` cannot contain a `copack`. There is algebraic equivalence between a hypothetical `pack` containing a `copack` (which is disallowed) and a specific shape of `copack` containing a `pack` — see Section 6 for details. + + +```cpp +struct IntegerToken {}; +struct StringToken {}; + +auto test_copack() -> void +{ + static constexpr fn::copack_for token = IntegerToken{}; + + // Member apply eliminates the copack by routing the active alternative to an overload set: + static constexpr auto value + = token.apply(fn::overload{[](IntegerToken) { return 1; }, [](StringToken) { return 2; }}); + static_assert(value == 1); + + // Storing a pack inside a copack is allowed, including a pack holding a reference: + auto cpr = fn::as_copack(fn::as_pack(value)); + static_assert(std::same_as>>); + + // Singular lift and direct value extraction (only allowed for singular copacks): + auto cp = fn::as_copack(42); + using std::get; + static_assert(std::same_as); +} +``` + +A key safety guarantee of `copack` is **exhaustive matching**. Operations that evaluate a `copack` (such as `transform` and `apply`) delegate to a multidispatch implementation that enforces compile-time exhaustiveness. If the callback or overload set fails to handle any possible alternative in the `copack`, the compilation is ill-formed. Direct `get` extraction is disallowed for multi-alternative `copack` types because the active alternative is a run-time property, meaning a multi-alternative `get` has no single static return type. Extraction must go through dispatch, which is exhaustive. + +## 5. Mapping values and errors + +Mapping changes contained data without altering the computation's structural success or failure shape. `libfn` uses `transform` (functor *map*) for the successful channel, and `transform_error` for the error channel. + + +```cpp +auto mapping_values_and_errors() -> void +{ + fn::expected> ex{}; + + auto mapped_val = ex | fn::transform([](UserId) { return User{}; }); + static_assert( + std::same_as>>); + + auto mapped_err = ex + | fn::transform_error(fn::overload{[](Missing) { return BadSyntax{}; }, + [](IoError e) { return e; }}); + + static_assert( + std::same_as>>); +} +``` + +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`. +- 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>` and `expected, E>` on the value side, and `expected>` on the error side. + +> [!TIP] +> +> ### Mathematical note — functorial action on the initial object +> +> In `libfn`, `transform` implements the functorial map ($fmap$). Its action on the initial object $0$ — the uninhabited `copack<>` — is forced rather than chosen: for every object $U$ there is exactly one morphism $0 \to U$, so a callback out of $0$ carries no information. Any two candidates denote the same morphism, and the result is determined without consulting either; Haskell spells this unique morphism `absurd :: Void -> a`. +> +> Nothing can therefore be asked of the callback — not even that it be callable, the same vacuity Section 10 describes for `or_else`. +> +## 6. Product composition with operator& (conjunction) + +Conjunction evaluates independent computations. `a & b` succeeds only if both operands succeed: values combine into a `pack`, and errors union into a `copack`. If both operands share the same error type, the error side remains ungraded. + + +```cpp +auto operator_and_composition(fn::expected a, fn::expected b) -> void +{ + auto result = a & b; + static_assert(std::same_as, fn::copack_for>>); +} +``` + +The runtime semantics are exact: + +- The result type records every error either operand can produce; at runtime it holds at most *one* of them — the leftmost failing operand's. +- Both operands are fully constructed before `operator&` runs, because C++ evaluates operands eagerly. This is an error-selection rule, not short-circuiting: the operator makes nothing lazy or parallel. + +### Conjunction over data + +Unlike disjunction (Section 7), `operator&` applies to payload types. When an operand is `copack`, it performs a Cartesian distribution, yielding a `copack` of `pack`s. A `pack` on the opposite side widens each of those `pack`s. + + +```cpp +auto test_cartesian_distribution(fn::copack_for ab, fn::copack_for cd) -> void +{ + // Cartesian distribution of copacks: (A + B) x (C + D) = (A x C) + (A x D) + (B x C) + (B x D) + auto result1 = ab & cd; + static_assert( + std::same_as, fn::pack, fn::pack, fn::pack>>); + + // Cartesian distribution of a pack and a copack: (A x B) x (C + D) = (A x B x C) + (A x B x D) + constexpr fn::pack Pab = {A{}, B{}}; + auto result2 = Pab & cd; + static_assert( + std::same_as, fn::pack>>); +} +``` + +A bare `scalar & scalar` is outside the algebra; it fails to compile for class types and resolves to the bitwise `AND` for built-in types like `int`. Conjunction dispatches on the left operand, so lifting one side—such as `fn::as_pack(a) & b`—enables the algebra. + +The n-ary fold `fn::conjoin(...)` operates in two modes: + +- If all arguments are computation carriers, it folds them as a monadic conjunction, equivalent to cascading `operator&`. +- If no arguments are carriers, it conjoins them as a data-level product. + +Mixing carriers and data in a single call is ill-formed. + +### Conjunction with the Identity Cluster + +An operand from the identity cluster (Section 10) contributes a value but never a failure: + +- **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>` 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` and `expected>` act as the product's identity unit and are elided from the value product (e.g., `expected & just` remains `expected`). +- **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. + + +```cpp +auto test_conjunction_with_identity_cluster(fn::expected ex, fn::just j) -> void +{ + // Conjoining an expected with a just + auto res1 = ex & j; + static_assert(std::same_as, Error>>); + + // Conjoining with a unit (just) completely elides the unit + auto res2 = ex & fn::just{}; + static_assert(std::same_as); + + // Conjoining a choice causes distribution inside the carrier + fn::choice ch = 1.5; + auto res3 = ex & ch; + static_assert(std::same_as< + decltype(res3), + fn::expected, fn::pack>, Error>>); +} +``` + +> [!TIP] +> +> ### Mathematical note — strict monoidal structure and distribution +> +> Conjoining independent computations via `operator&` models a symmetric monoidal category $(\mathcal{C}, \otimes, I)$, with `pack` as the tensor $\otimes$ and `pack<>` as the unit $I$. +> +> - **Strictness**: because packs are normalized flat, there are no distinct-but-isomorphic spellings for the coherence maps to mediate between. $(A \otimes B) \otimes C$ and $A \otimes (B \otimes C)$ are *the same C++ type*, as are $I \otimes A$, $A \otimes I$ and $A$. The associator and unitors are therefore identities, and the structure is **strict** rather than merely coherent. +> - **Symmetry**: for the sum it is strict too — canonical ordering makes $A \oplus B$ and $B \oplus A$ literally the same `copack`, so the braiding is an identity. For the product it is not: a `pack` keeps its fields ordered and undeduplicated, so $A \otimes B$ and $B \otimes A$ are distinct types related by a genuine swap. +> - **Distributivity**: the tensor distributes over the coproduct $\oplus$ (represented by `copack`), yielding $A \otimes (B \oplus C) \cong (A \otimes B) \oplus (A \otimes C)$. This is the Cartesian distribution of `pack` over `copack` implemented statically by `libfn`. +> +## 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<>`. + +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. + + +```cpp +auto operator_or_composition(fn::expected a, fn::expected b) -> void +{ + auto result = a | b; + static_assert(std::same_as, fn::pack>>); +} +``` + +The runtime semantics are exact: + +- The result type records every combination of failures; at runtime the leftmost operand holding a value wins. +- Both operands are fully constructed before `operator|` runs, because C++ evaluates operands eagerly. This is a value-selection rule, not a lazy fallback. + +### Disjunction over carriers only + +Unlike conjunction, disjunction has no data-level form. Neither `operator|` nor the n-ary fold `fn::disjoin(...)` accepts a `pack`, a `copack`, or a scalar. Therefore, built-in operations (such as bitwise `OR` on integers) are never confused with disjunction. + +### Disjunction with the Identity Cluster + +When an operand belongs to the identity cluster (Section 10), the disjunction cannot fail: + +- **Error Annihilation**: The error side gains an uninhabited factor (`copack<>`), which annihilates the error product and collapses the channel. +- **Infallible Folding**: The result folds into a non-failing carrier: `just` if there is a single successful type, or `choice` if the sum is heterogeneous. + + +```cpp +auto test_disjoin(fn::expected a, fn::expected b) -> void +{ + // Multiple fallible operands compose cleanly + auto res1 = fn::disjoin(a, b); + static_assert(std::same_as, fn::pack>>); + + // Because just cannot fail, the entire disjunction with infallible operands becomes total + auto res2 = fn::disjoin(a, b, fn::just{1.5}); + static_assert(std::same_as>); +} +``` + +> [!TIP] +> +> ### Mathematical note — annihilation and totality +> +> Disjunction is the categorical dual of conjunction: values add along $\oplus$ where conjunction multiplies along $\otimes$, and errors multiply where conjunction unions. Both structures are strict in the sense Section 6 describes. +> +> What does *not* dualize is the role of the uninhabited `copack<>`, and that is what makes an identity operand behave so differently on the two sides. Under conjunction it is the unit of the error union, $E \cup 0 = E$, so an identity operand leaves the error channel as it found it. Under disjunction it is the **annihilator** of the error product, $E \otimes 0 \cong 0$, so a single identity operand empties the channel outright. +> +> The resulting error side is uninhabited by construction — not merely empty at runtime, but incapable of holding a value — which is what renders the whole disjunction total and folds it into the identity cluster. +> +## 8. Sequential composition with and_then + +Sequential composition chains dependent operations: the success of one feeds the input of the next. In `libfn`, `and_then` (monadic *bind*) achieves this. + +A monadic carrier wraps a value. The callable passed to `and_then` is a *Kleisli arrow*; it accepts a plain value and returns a monadic carrier of the same kind. If the input is infallible, it may return either an infallible carrier or a fallible kind it bridges to (Section 10). + +A chained sequence of *bind* operations forms the graded pipeline introduced in Section 1. Chaining a single step follows this pattern: + + +```cpp +auto parse_numeric() -> fn::expected>; +auto load_user(UserId) -> fn::expected>; + +auto sequential_bind() -> void +{ + auto result = parse_numeric() | fn::and_then(load_user); + static_assert( + std::same_as>>); +} +``` + +Because member `.and_then` cannot change carrier families (Section 3), its *Kleisli arrow* must return the same carrier family: + +- `optional` binds to `optional`. +- Plain `expected` binds to `expected` (retaining error `E`) or, via singular lift, to `expected>` (transitioning to a graded context, Section 9). +- 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. +- 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: + + +```cpp +static_assert(fn::same_kind, fn::optional>); +static_assert(fn::same_kind, fn::expected>); +static_assert(!fn::same_kind, fn::expected>); +``` + +## 9. Graded expected: exact error sets + +Grading an `expected` provides exactly bounded error sets. When an outer computation holds a coproduct of successful values, and each value requires a different operation to proceed, `libfn` derives a single, normalized `expected` shape. + +Consider a configuration reader that parses a loosely typed file into specific valid structural alternatives: `MaximumSize`, `FilePath`, or `BlockSize`. + + +```cpp +auto read_config() -> fn::expected, + fn::copack_for>; + +auto config_pipeline() -> void +{ + auto validated + = read_config() + | fn::and_then(fn::overload{ + [](MaximumSize v) { return fn::expected>{v}; }, + [](FilePath v) { return fn::expected>{v}; }, + [](BlockSize v) { return fn::expected>{v}; }}); + + // The result exactly bounds both the successful paths and the error paths + static_assert( + std::same_as, + fn::copack_for>>); +} +``` + +Two independent joins occur during `and_then`: + +1. The successful branch values formed the normalized value copack. +2. The existing outer errors (`BadSyntax`, `UnknownKey`) and the new branch errors (`OutOfRange`, `Missing`) formed the normalized error copack. + +Unioning allows different grades of `expected` to share the same carrier family. Although ungraded `expected` requires the identical error type `E` (or its singular lift `copack`) to participate in monadic *bind*—meaning `expected` and `expected` are not `same_kind`—any two graded `expected` types are `same_kind` regardless of how their error sets differ, as the compiler can always derive their union: + + +```cpp +static_assert( + fn::same_kind>, fn::expected>>); +``` + +Value joining and error grading are independent: branch values can join while the error side stays plain, as in `expected, E>`. + +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` if the returning **error type** of the callback is `copack`. +- In `or_else` (recovery/error binding), a plain success type `T` is promoted to `copack` if the returning **success type** of the callback is `copack`. + +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` to `expected>`. +- `.copack_value()` on `expected` explicitly lifts the success value, transforming `expected` to `expected, E>`. +- `.copack_value()` on `optional` symmetrically lifts the value, transforming `optional` to `optional>`. + +These helper methods provide a compact, explicit alternative to the pipeline promotions: + + +```cpp +auto test_explicit_lifting(fn::expected result, fn::optional opt) -> void +{ + // Explicitly lift the error side of expected: + auto graded_err = std::move(result).copack_error(); + static_assert(std::same_as>>); + + // Explicitly lift the value side of expected: + auto graded_val = std::move(result).copack_value(); + static_assert(std::same_as, IoError>>); + + // Explicitly lift the value side of optional: + auto graded_opt = std::move(opt).copack_value(); + static_assert(std::same_as>>); +} +``` + +Recovery via `or_else` behaves symmetrically. It handles input error alternatives and joins any new errors produced by the recovery branches, while preserving the successful value path. Heterogeneous recovery values require a suitable copack-valued input. Any original error handled by a branch is removed from the resulting grade unless a branch explicitly re-returns it. When there are no error alternatives to handle—such as in `expected>`—there is nothing to recover from: the callback is neither invoked nor instantiated. + +### Widening is subeffecting + +In accordance with the subeffecting principles of graded monads (Section 1), a narrow error set can be widened during composition, but narrowing requires explicit mitigation. Implicit narrowing (without handling the removed errors) is unsafe and rejected by the compiler. +However, you can safely narrow or collapse an error grade by explicitly handling and mapping the errors using `transform_error`. Because `transform_error` on a graded `expected` forces exhaustive matching, you can map diverse error types into one common error type (collapsing the grade to a singular `copack`) or into a narrower `copack`, safely reducing the static error grade. + +The bottom error grade is `copack<>`: + +```cpp +template +using cannot_fail_t = fn::expected>; +``` + +This computation cannot fail, but it is algebraically prepared to widen if later composition introduces possible errors. + +A concrete example of this is `expected>` (aliased as `fn::expected_unit` in the library). Because `void` represents the unit `1` and `copack<>` represents the zero `0`, this type maps algebraically to $1 + 0 \cong 1$. Having a cardinality of exactly one, it has no possible errors, can never fail, and can only succeed with a single empty trigger (`void`). This makes it structurally isomorphic to the **unit type**. + +In practice, `expected>` acts as the entry point for graded pipelines. Initiating a chain with this unit trigger opts subsequent `and_then` bindings into graded error-set unioning without requiring mock starting errors or manual wrapping. Because its starting error set is empty (`copack<>`), unioning it with subsequent steps' errors (such as `IoError`) yields exactly those errors. An alternative unit type without an error channel is `just` (Section 10). + +> [!TIP] +> +> ### Mathematical note — the graded functor and its two units +> +> Having established the error pomonoid $(\mathcal{E}, \cup, \emptyset, \subseteq)$ in Section 1, `libfn`'s graded `expected` is the **lax monoidal functor** $G : \mathcal{E} \to [\mathcal{C}, \mathcal{C}]$ from the pomonoid category $\mathcal{E}$ to the endofunctor category on C++ types, with $G_E(A) \cong \text{expected}\langle A, E\rangle$ (following Orchard, Wadler, and Eades, *Unifying graded and parameterised monads*). Its unit is +> +> $$\eta_A : A \to G_I(A), \qquad I = \emptyset$$ +> +> Two different units meet in the gateway type `expected>`: the neutral grade $I = \emptyset$ of the error pomonoid, and the unit object $1$ of the value category, spelled `void`. It is precisely $G_I(1)$. +> +## 10. The identity cluster + +Certain operations behave like an identity functor across carriers. Because some states correspond structurally, `libfn` licenses specific cross-carrier behavior to avoid boilerplate. + +The identity cluster consists of three structurally equivalent, infallible states: + +| Carrier | Algebraic State Shape | +| - | - | +| `just` | **T** (A single value) | +| `choice` | **Ts...** (A coproduct of values) | +| `expected>` | **T + 0** ≅ **T** (A value and an uninhabited error) | + +Each carrier is canonically isomorphic to its payload: none adds failure or empty states, so a value is always present. + +Because no member can hide failure, pipeline bind operations can transition across these boundaries: + + +```cpp +auto test_identity_cross() -> void +{ + fn::just j{UserId{}}; + + // Cross-carrier pipeline bind to another identity carrier + auto result = j | fn::and_then([](UserId u) { return fn::expected>{u}; }); + + static_assert(std::same_as>>); +} +``` + +The *bind* operation adopts the carrier family of the provided callback — a crossing only the pipeline-scoped functors are licensed to make (Section 3). + +### Success-Path Bridging + +Fallible carriers (excluding `expected>`) and `optional` cannot transition to infallible carriers, because doing so would risk silently discarding an active error or empty state. + +In contrast, identity carriers can bridge to any fallible carrier via pipeline `fn::and_then`. Because identity carriers are statically infallible, transitioning to `optional` or a standard `expected` merely introduces potential downstream failure. No pre-existing failure is discarded, as none can exist upstream: + + +```cpp +auto test_success_bridge(fn::just j) -> void +{ + // An identity carrier can bridge to fallible carriers on the success path + auto to_opt = j | fn::and_then([](int i) { return fn::optional{i}; }); + static_assert(std::same_as>); + + auto to_exp = j | fn::and_then([](int i) { return fn::expected{i}; }); + static_assert(std::same_as>); + + // Bridging a multi-alternative choice to fallible optional with heterogeneous success join: + auto choice_to_opt + = fn::choice_for{true} + | fn::and_then(fn::overload{[](int) -> fn::optional { return {'a'}; }, + [](bool) -> fn::optional { return {2L}; }}); + static_assert(std::same_as>>); +} +``` + +All three cluster members (`just`, `choice`, and `expected>`) can bridge to fallible carriers. + +Monadic operations on the identity cluster: + +- **Success Mapping (`transform`)**: Preserves the carrier family for member calls. Pipeline `fn::transform` adds a licensed crossing: a `copack` returned from a callable mapped over a `just` is promoted to a `choice` over the same alternatives (Section 11). +- **Sequential Binding (`and_then`)**: Allows cross-carrier transitions within the identity cluster (such as `just` to `expected>`) when using pipeline `fn::and_then`. +- **Recovery & Error Mapping (`transform_error`, `or_else`, `recover`, `inspect_error`)**: Because `just` and `choice` lack an error side, these are ill-formed. On `expected>`, they are vacuously well-formed but statically unreachable to allow generic compilation. +- **Short-Circuiting (`fail`, `filter`)**: Ill-formed for identity carriers, as no failure (error or empty state) can be constructed from an infallible context. +- **Elimination Fallbacks (`value_or`)**: Ill-formed on `just` and `choice`. On `expected>`, it remains well-formed to support generic code, but the fallback must still initialize `T` even though its branch is statically unreachable. If the value side is `void`, no fallback is accepted. +- **Neutral Observation (`inspect`, `discard`)**: Supported and behave normally. + +> [!NOTE] +> +> ### Note — the vacuous `or_else` asks nothing +> +> For instance, the following function compiles successfully, even though the recovery handler is a plain `int` (not a callable at all): +> +> +> ```cpp +> auto test_vacuous_or_else() -> void +> { +> using type = decltype(fn::expected>{} | fn::or_else(std::declval())); +> static_assert(std::same_as>>); +> } +> ``` +> +> In contrast, if the error grade is inhabited (or on `optional`, where the empty state is a genuine inhabited state), `or_else(42)` is loudly rejected. +> +> The `or_else` operation evaluates the callback over the error alternatives. Over the uninhabited `copack<>`, there are zero alternatives, so the underlying fold has zero inputs. The operation trivially collapses to the identity mapping, the callback contributes nothing, and no questions about the callback — not even invocability — are formable. Demanding a constraint on the callback would be an arbitrary invention rather than a logical derivation. +> +> This serves a load-bearing design principle: generic code remains closed under all error grades. If `or_else` were rejected on `expected>`, a recovery step would become ill-formed simply because an upstream stage statically proved that failure is impossible, breaking generic composition. Instead, the recovery step stays writable everywhere—and does nothing where failure is impossible. + +> [!TIP] +> +> ### Mathematical note — isomorphic yet nominally distinct +> +> The three shapes in the table are isomorphic in $\mathcal{C}$, so they carry the same information. C++ is nominal, and `libfn` keeps it that way rather than exposing those isomorphisms as implicit conversions: doing so would drop three mutually convertible types into every overload set that mentions any one of them, and the conversions would compose into cycles. Each isomorphism is instead reachable as a **licensed crossing** — a pipeline functor you invoke — so the equivalence is available exactly where it is asked for. +> +> That boundary is not only a restriction: in Section 11 it is what makes `choice` a monad rather than a bare coproduct. +> +## 11. choice: identity over a coproduct + +`choice` represents a computation that always succeeds by selecting one of several alternatives. Structurally, it serves as the single-layer carrier for coproduct states, avoiding the invalid nested `just>` representation (Section 3). + +### Promotion via Pipeline Functors + +Pipeline `fn::transform` on a `just` that returns a `copack` promotes automatically to `choice`: + + +```cpp +auto test_identity_transformation(fn::just j) -> void +{ + // Transforming a just with a callable returning a copack produces a choice + auto mapped + = j | fn::transform([](UserId) { return fn::copack_for{Missing{}}; }); + + static_assert(std::same_as>); +} +``` + +Similarly, pipeline `fn::and_then` on a `just` can return a `choice` or `expected>` directly. + +Inside its domain, `choice` behaves differently from bare `copack`: + +- `copack` is self-flattening data; a `copack` returned from a branch dissolves into the result. +- `choice` is an atomic, never-failing computation; a `choice` returned from a branch remains an alternative unless `and_then` joins it away. + +Consider a scenario where different branches of a switch return different `choice` types: + + +```cpp +auto test_choice_mapping(fn::choice ch) -> void +{ + constexpr auto mapper = fn::overload{[](UserId) { return fn::choice{Missing{}}; }, + [](User) { return fn::choice{FilePath{}}; }}; + + // transform nests the returned choice as a mapped value + auto mapped = ch | fn::transform(mapper); + static_assert( + std::same_as, fn::choice>>); + + // and_then joins and flattens them into a normalized superset choice + auto bound = ch | fn::and_then(mapper); + static_assert(std::same_as>); +} +``` + +A callback returning a bare value requires `transform` rather than `and_then`; `choice`'s `and_then` rejects bare value returns with a compile-time diagnostic. + +> [!TIP] +> +> ### Mathematical note — why copack is not a monad, but choice is +> +> Categorically, `copack` is an object-level **coproduct** (disjoint sum $\bigoplus T_i$), whereas `choice` is a **monad** representing a coproduct-bearing computation context. +> +> 1. **`copack` is self-flattening (not a monad)**: +> Naked sums are naturally self-flattening (e.g., $(A + B) + C \cong A + B + C$), and `libfn` enforces it syntactically: a nested `copack` is ill-formed by design. But the multiplication $\mu_A : M(M(A)) \to M(A)$ presupposes that $M \circ M$ is expressible as a type, so *join* has no domain to act on. The constraint is intentional: it allows `transform` on `copack` to collapse every branch's results into a flat, deduplicated set instead of an ever-nesting type. +> +> 2. **`choice` is the monad (the "structural suspend button")**: +> To restore monad laws, the monadic carrier `choice` wraps the sum in an "identity layer" to preserve structural depth: `choice>` $\ne$ `choice`. That layer holds eager flattening in check. +> Thus, `choice` acts as a lawful monad under $M(A) = \text{choice}\langle A\rangle$ over coproduct objects $A = \bigoplus_{j} T_j$ — the *nominal* identity wrapper, which Haskell spells as the `Identity` newtype. Because $M(A) \cong A$ yet is a distinct C++ type, the unit and the multiplication are exactly the wrapping and unwrapping that nominal typing makes observable: +> - **Unit / return** $\eta_A : A \to M(A)$ : wraps the coproduct in the `choice` layer. Building a `choice` from a single alternative composes this with the coproduct's own injection $\iota_i : T_i \to \bigoplus_j T_j$. +> - **Join / flatten** $\mu_A : M(M(A)) \to M(A)$ : strips one `choice` layer, letting the underlying sum deduplicate its alternatives (the codiagonal fold $[id, id]$, executed statically via `choice_for`). +> - **Bind**: maps, then flattens explicitly via *join*. Making that step explicit is what grants control over *when* flattening occurs. +> +> +## 12. Elimination and multidispatch + +Eliminating computation structures yields an ordinary C++ value. This is typically achieved via `apply` or `apply_r`. Singular `copack` supports direct extraction via `get` (Section 4), and `just` supports total value extraction via `.value()`. On fallible carriers, `.value()` is partial: it yields the value if present, and otherwise throws (`bad_expected_access`, `bad_optional_access`). + +`transform` and `apply` differ in whether the structure survives: + +- `transform` remains inside the carrier or `copack`, producing a new carried type. +- `apply` eliminates the structure: the result type is deduced from the branches, which must all return the same type. +- `apply_r` permits any branch results that convert to `R`. + +> [!NOTE] +> +> ### Note — eliminating with heterogeneous branches +> +> Because `apply` leaves the algebra, it must deduce a single result type; branches that disagree are rejected. When branch types differ, specifying a target `copack` via `apply_r>` accepts branches returning `A`, `B`, or `C`, as each alternative converts implicitly into the parent `copack`. + +Application expands one selected level only. The call shapes are straightforward: + +| Type | Eliminated Call Shape | +| - | - | +| `A` | `f(A)` | +| `pack` | `f(A, B)` | +| `pack<>` | `f()` | +| `std::tuple` | `f(A, B)` | +| `copack` | `f(A)` or `f(B)` | +| `copack, C>` | `f(A, B)` or `f(C)` | + +A whole-carrier `expected` application cleanly handles both success and error paths into one result type: + + +```cpp +auto test_elimination(fn::expected> ex) -> int +{ + return ex.apply(fn::overload{[](UserId) { return 1; }, [](Missing) { return 0; }}); +} +``` + +Exhaustiveness is statically constrained. If you omit a handler for a possible type, the compilation fails. `fn::overload` is merely a helper; final selection always relies on ordinary C++ overload resolution. + +> [!WARNING] +> +> ### Warning — catch-all handlers defeat exhaustiveness +> +> Exhaustiveness is checked against the types your handlers name. An unconstrained `[](auto)` names all of them, so a `copack` that later gains an alternative still compiles — the new alternative routes into the catch-all instead of failing where its branch is missing. Where one handler should serve several alternatives, name a concept instead of `auto` — for example `[](MyConcept auto &&)` — and the check still fires for anything the concept does not admit. + +### Type-tagged elimination + +Because storage shape and call shape are distinct, untagged `apply` can erase structural context (such as in `expected`). To preserve context and prevent implicit conversions from conflating different states, `libfn` provides **`apply_type`** (and `apply_type_r`) member functions. + +Eliminating a carrier using `apply_type` passes an explicit state tag as the first argument to the handler, followed by the unpacked payload: + +- **`expected`**: The success arm receives `std::in_place` and the success value (or `std::in_place` alone if `void`), while the error arm receives `fn::unexpect` and the error. +- **`optional`**: The success arm receives `std::in_place` and the value, while the empty arm receives `std::nullopt`. +- **`copack` & `choice`**: The active alternative arm receives `std::in_place_type` and the payload. +- **`just`**: The arm receives `std::in_place_type` and the value. Symmetrically, `just`'s arm receives `std::in_place_type` alone, representing a nullary unit payload. + +> [!TIP] +> +> ### Mathematical note — elimination of algebraic structures +> +> The two eliminations are not dual to one another — the dual of coproduct elimination is product *introduction*, pairing — and they differ in how much the category determines. +> +> - **Coproduct**: eliminating $A + B$ is canonical. Supplying $f : A \to C$ and $g : B \to C$ determines a *unique* mediating morphism $[f, g] : A + B \to C$ by the universal property, and in `libfn` ordinary C++ overload resolution is what computes it. +> - **Product**: eliminating $A \times B$ invokes no universal property — a morphism out of a product is just a morphism. What `apply` supplies is the bridge between the product as *stored*, a `pack`, and the product as an argument list, which C++ keeps distinct: it uncurries. +> +> Carrier elimination (`apply_type`) additionally preserves the injections: the state tag — such as `std::in_place`, `fn::unexpect` or `std::in_place_type` — tells the handler *which* injection morphism placed the value into the structure. +> +## 13. The monadic operations map + +A reference of `libfn` operations, organized by channel and effect: + +**Success Channel** + +- `transform`: Maps the successful value. Stays inside the carrier. +- `and_then`: Sequences success-path computations. The mechanism for introducing new errors into a graded expected. +- `filter`: Enters a short-circuit state if a predicate fails. +- `inspect`: Observes the successful value transparently. +- `fail`: Intercepts success and forces a transition to a failure state. + +**Error/Empty Channel** + +- `transform_error`: Maps the error value. Stays inside the carrier, and is the one operation that can narrow a graded error set (Section 9). +- `or_else`: Sequences computations based on errors. Joins recovery values. +- `recover`: Intercepts failure and forces a transition back to a success state. +- `inspect_error`: Observes the error value transparently. +- `value_or`: Supplies a fallback. Member `.value_or(x)` eliminates the carrier to return the value; pipeline `fn::value_or(x)` preserves the carrier, returning it engaged with either its own value or the fallback. + +**Neutral** + +- `discard`: Unconditionally evaluates the carrier, discards the result, and returns `void` to signal that the return value is deliberately ignored. + +**Elimination** + +- `apply`: Routes the stored state to an overload set, leaving the algebra with an ordinary C++ value. +- `apply_type`: The same elimination, keyed by an explicit state tag. + +**Composition & Combination** + +- `operator&` (conjunction): Combines independent computations (values into a `pack`, errors as a union). +- `operator|` (disjunction): Combines alternative computations (values into a `copack` disjoint sum, errors as a product `pack`). +- `fn::conjoin`: An n-ary fold of `operator&`, over monadic carriers or over packs, copacks and scalars — not a mix of the two. +- `fn::disjoin`: An n-ary fold of `operator|` over monadic carriers, supporting total disjunction with the identity cluster. + +### Key Architectural Rules of the Map + +- **`fail` and `recover` are duals**: `fail` transitions success to failure ($Success \implies Failure$), and `recover` transitions failure to success ($Failure \implies Success$). Neither operation widens a graded error set. +- **Graded `and_then`**: Widens the error grade by introducing new error types into the pipeline. +- **`filter` and `fail`**: Enter an existing short-circuit state; the carrier must already support the failure state. +- **Error-side operations** (such as `transform_error`, `or_else`, `recover`, and `inspect_error`): Require a carrier with an error or empty side. They are ill-formed on `just` and `choice`, and remain vacuously well-formed on `expected>` (Section 10). + +## 14. Laws as C++ equalities + +Where the carried types compare equal in a constant expression, the laws are checked by the compiler itself. Functor identity and monad left identity are machine-checked below; the remaining laws hold structurally, by construction of the derived types: + + +```cpp +constexpr auto test_laws() -> void +{ + constexpr fn::expected> ex{42}; + + // Functor Identity: mapping with identity yields the same value + constexpr auto id = [](auto v) { return v; }; + static_assert((ex | fn::transform(id)) == ex); + + // Monad Left Identity: pure(x) >>= f is equivalent to f(x) + constexpr auto pure = [](int v) { return fn::expected>{v}; }; + constexpr auto f = [](int v) { return fn::expected>{v * 2}; }; + static_assert((pure(42) | fn::and_then(f)) == f(42)); +} +``` + +Other properties hold structurally: + +- **Functor composition**: `m | transform(f) | transform(g)` equals `m | transform([](auto v) { return g(f(v)); })`. +- **Monad right identity**: `m | and_then(pure)` equals `m`. +- **Monad associativity**: `(m | and_then(f)) | and_then(g)` equals `m | and_then([](auto v) { return f(v) | and_then(g); })`. For graded expected, both sides of the associativity derive the same normalized union grade. +- **Product associativity**: Holds after canonical `pack` normalization. +- **Coproduct set semantics**: Union associativity, commutativity, and idempotence apply. +- **Coherent widening**: Upcasting an error through intermediate supersets yields the same final type as upcasting directly to the broadest superset. +- **Identity cluster binds**: Laws hold across `just`, `choice`, and `expected>` via the canonical payload-preserving state-shape correspondence. + +## 15. C++ mechanics that preserve the algebra + +To ensure reliability, `libfn` uses compiler mechanisms to reject malformed usage and preserve performance. + +### Constraints and Exhaustiveness + +Public concepts and `requires` clauses enforce correctness before instantiation. Operations are protected by public applicability concepts (such as `fn::applicable_transform` and `fn::applicable_and_then`) that evaluate to `false` for invalid calls rather than triggering deep compiler errors. This underpins the compile-time exhaustiveness guarantees of `apply` and monadic operations established in Sections 4 and 12. + +### C++ value properties + +The library respects C++ value mechanics: + +- Core operations are `constexpr`. +- The algebra's own types (`pack`, `copack`, `just`, and `choice`) are structural when their elements are, so a `constexpr` value of these types may be used as a template parameter. +- `noexcept` is conditionally computed. +- Value categories (lvalue/rvalue) propagate strictly to callbacks, avoiding copies. +- Immovable and move-only payloads are supported in place. +- Reference-bearing `pack` and `optional` are supported. Lifetime management of non-owning references remains with the caller. +- `pack` compares element-wise, supporting equality and three-way comparison. For reference-bearing `pack`, comparison applies to the referents rather than the references themselves. + +> [!NOTE] +> +> ### Note — reference payloads +> +> Raw reference payloads are disallowed on the carriers `expected`, `just` and `choice`, and as `copack` alternatives. `expected` stores its payload in a union, and C++ forbids a union member of reference type; the algebra's own types refuse them so that every alternative is dispatched the same way, whatever it holds. `optional` is the deliberate exception — the standard specifies it, and `libfn` polyfills it. If you want to propagate references inside the other carriers, wrap them in a `pack` (e.g. `expected, E>`). + + +```cpp +auto test_references() -> void +{ + int x = 42; + + // optional supports references directly + fn::optional opt{x}; + static_assert(std::same_as); + + // expected must wrap references inside a pack + fn::expected, Error> ex{fn::as_pack(x)}; + static_assert(std::same_as &>); +} +``` + +### pfn and fn + +The library is divided into layers: + +- `pfn` (Polyfill fn) is the standards-facing layer. It provides `std::optional` and `std::expected` in their C++26 shape — monadic member functions, `optional`, range support — plus smaller utilities such as `std::invoke_r` and `std::unreachable`, all available to a C++20 compiler. +- `fn` is the strict extension layer. It introduces the `pack`/`copack` algebra, multidispatch, graded errors, `choice`, `just`, the pipeline functors, and the composition operators `&` and `|`. + +Every `fn` type with a `pfn` counterpart is a strict superset of it: switching a valid program from `pfn` to `fn` changes neither compilation nor behaviour. + +## Functional terminology + +For readers with a background in functional languages (like Haskell or OCaml), this table translates standard terminology to `libfn`'s C++ vocabulary: + +| Functional Term | libfn Equivalent | +| --------------- | ------------------ | +| `fmap` / `map` | `transform` / `transform_error` | +| `bind` / `>>=` | `and_then` | +| `pure` / `return` | `just{v}` / `expected>{v}` — a carrier constructor | +| Lift / inject | `fn::as_pack` / `fn::as_copack` | +| Kleisli arrow | The callable passed to `and_then` | +| Product type | `pack` / `std::tuple` | +| Coproduct / Sum | `copack` (the sum itself) / `choice` (the never-failing carrier over a sum) | +| Subeffecting | Widening an error grade / subset inclusion | + +## Further reading + +For formal validation of the algebraic structures modeled in `libfn`, refer to: + +1. Orchard and Petricek, [“Embedding effect systems in Haskell”](https://www.doc.ic.ac.uk/~dorchard/publ/haskell14-effects.pdf) (for effect sets, union, and subeffecting). +2. Orchard, Wadler, and Eades, [“Unifying graded and parameterised monads”](https://arxiv.org/pdf/2001.10274) specifically Definition 21 (for the graded-monad interpretation). +3. McDermott and Uustalu, [“Flexibly Graded Monads and Graded Algebras”](https://dylanm.org/flexibly-graded-monads.pdf) _Note: `libfn` does not claim to fully implement their flexibly graded construction, but the work contextualizes graded structures._ diff --git a/cmake/Docs.cmake b/cmake/Docs.cmake index 52610df3..c950bad9 100644 --- a/cmake/Docs.cmake +++ b/cmake/Docs.cmake @@ -1,9 +1,17 @@ find_package(Doxygen REQUIRED) find_package(Znai REQUIRED) +find_package(Python3 COMPONENTS Interpreter REQUIRED) set(DOXYGEN_GENERATE_HTML NO) set(DOXYGEN_GENERATE_XML YES) +# doxygen records a trailing return type verbatim, so DEDUCED_RETURN would reach the API +# reference as itself. Expanding only what is named here substitutes the form every compiler +# but MSVC sees, and leaves every other macro to be read as written. +set(DOXYGEN_MACRO_EXPANSION YES) +set(DOXYGEN_EXPAND_ONLY_PREDEF YES) +set(DOXYGEN_PREDEFINED "DEDUCED_RETURN(x)=decltype(auto)" "explicit(x)=explicit") + # Doxygen reads a staged copy of include/ with the ABI inline namespace stripped, so the XML # carries the API exactly as readers spell it (fn::X, pfn::X), independent of doxygen's and # znai's inline-namespace handling. @@ -23,6 +31,8 @@ macro(znai_export_docs TARGET SOURCE_DIR DEPLOY_DIR) add_custom_target( ${TARGET} COMMAND ${Znai} --source ${SOURCE_DIR} --deploy ${DEPLOY_DIR} --doc-id '""' --lookup-paths ${CMAKE_BINARY_DIR} + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/fix_site_urls.py" "${DEPLOY_DIR}" COMMENT "Exporting documentation to ${DEPLOY_DIR}" ) endmacro() @@ -34,11 +44,48 @@ doxygen_add_docs( ) add_dependencies(docs_xml docs_stage_include) +# znai reads a staged copy of docs/, which carries a chapter per root document alongside the +# API reference; the root documents themselves stay whole, and out of the site's source. +set(docs_staged_source ${CMAKE_BINARY_DIR}/docs_source) +add_custom_target(docs_stage_source + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/stage_docs_source.py" + "${CMAKE_CURRENT_SOURCE_DIR}" + "${docs_staged_source}" + COMMENT "Stage docs/ with a chapter per root document" + VERBATIM +) + +# A reference page presents an overload set the way cppreference does, as one listing of +# signatures - which znai cannot draw, its doxygen node carrying no ref-qualifier. The +# listings are therefore written out in the pages, and this holds them to the headers. +add_custom_target(docs_check_signatures + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/doxygen_signatures.py" check + --xml "${CMAKE_BINARY_DIR}/xml" + --docs "${CMAKE_CURRENT_SOURCE_DIR}/docs/reference" + COMMENT "Check the reference signature listings against the headers" + VERBATIM +) +add_dependencies(docs_check_signatures docs_xml) + +# znai renders a description it cannot find as silence, so a page can lose one without the +# build noticing. This holds what the headers document to what the site actually reaches. +add_custom_target(docs_check_coverage + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_docs_coverage.py" + --xml "${CMAKE_BINARY_DIR}/xml" + --docs "${CMAKE_CURRENT_SOURCE_DIR}/docs" + COMMENT "Check every documented entity reaches the documentation" + VERBATIM +) +add_dependencies(docs_check_coverage docs_xml) + znai_export_docs( export_docs - ${CMAKE_CURRENT_SOURCE_DIR}/docs + ${docs_staged_source} ${CMAKE_BINARY_DIR}/docs COMMENT "Export final documentation" ) -add_dependencies(export_docs docs_xml) +add_dependencies(export_docs docs_xml docs_stage_source docs_check_signatures docs_check_coverage) diff --git a/docs/choice/and_then.md b/docs/choice/and_then.md deleted file mode 100644 index 7cf14e35..00000000 --- a/docs/choice/and_then.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: fn::and_then ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::and_then_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::and_then_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-choice-parse", "// example-choice-checks"] -} diff --git a/docs/choice/index.md b/docs/choice/index.md deleted file mode 100644 index 7d1af1fa..00000000 --- a/docs/choice/index.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Choice monad ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::choice - -## choice_for {style: "api"} -The construction alias: accepts alternatives in any order, with duplicates and nested copacks, -and resolves to the canonical `fn::choice`. Prefer it over spelling `choice` directly, so that no -spelling in your project is tied to one compiler's alternative order. - -:include-doxygen-member: fn::choice_for { signatureOnly: false, includeAllMatches: true } - -## choice {style: "api"} -Construction: from a value of one alternative, in place from arguments, or widening from a -`copack` over a subset of the alternatives. - -:include-doxygen-member: fn::choice { signatureOnly: false, includeAllMatches: true } - -## value {style: "api"} -The alternatives as the underlying `copack`, always present. - -:include-doxygen-member: fn::choice< Ts... >::value { signatureOnly: false, includeAllMatches: true } diff --git a/docs/choice/transform.md b/docs/choice/transform.md deleted file mode 100644 index 5dcc9e8d..00000000 --- a/docs/choice/transform.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::transform ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::transform_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::transform_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/ci/index.md b/docs/ci/index.md deleted file mode 100644 index 88218e3a..00000000 --- a/docs/ci/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Continuous Integration ---- - -##### Workflows - -Workflows for continuous integration: - -* `build` - check build for `gcc` and `clang` compilers on [Debian Linux](https://debian.org/) -* `nix` - check build on [nix](https://nixos.org/) platform -* `license` - license scan by [FOSSA](https://app.fossa.com/projects/git%2Bgithub.com%2Flibfn%2Ffunctional) -* `pre-commit` - enforce rules defined in `.pre-commit-config.yaml`, including `clang-format` -* `codecov` - submit unit tests coverage to [codecov.io](https://app.codecov.io/gh/libfn/functional) -* `docs` - build this documentation site [libfn.org](https://libfn.org/) -* `ci-...` - build [docker images](https://hub.docker.com/r/libfn) for continuous integration - -##### Images - -Images for continuous integration are defined in: - -* `ci/build/gcc` - [GCC compiler](https://gcc.gnu.org/) for `build` and `coverage` workflows -* `ci/build/clang` - [Clang compiler](https://clang.llvm.org/) for `build` workflow -* `ci/pre-commit` - [pre-commit](https://pre-commit.com/) for `pre-commit` workflow -* `ci/docs` - [Znai](https://testingisdocumenting.org/znai/) for `docs` workflow - -Images are refreshed at least once a month by `ci-...` workflows diff --git a/docs/composition/index.md b/docs/composition/index.md deleted file mode 100644 index ad32abc2..00000000 --- a/docs/composition/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Composition ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -Independent computations compose side by side. The conjunction `a & b` keeps both results: -values multiply into a `pack` and errors sum into a `copack`, with the leftmost failing operand's -error held at runtime. The disjunction `a | b` keeps the first that worked: values sum into a -`copack` and errors multiply into a `pack`, present only when every operand failed. Each -carrier's header declares its own `&` and `|` operators; this header defines their n-ary folds -`fn::conjoin` and `fn::disjoin`. - -## conjoin {style: "api"} -:include-doxygen-doc: fn::conjoin_t - -## conjoin call signatures {style: "api"} -:include-doxygen-member: fn::conjoin_t::operator() { signatureOnly: false, includeAllMatches: true } - -## disjoin {style: "api"} -:include-doxygen-doc: fn::disjoin_t - -## disjoin call signatures {style: "api"} -:include-doxygen-member: fn::disjoin_t::operator() { signatureOnly: false, includeAllMatches: true } diff --git a/docs/continuous-integration/index.md b/docs/continuous-integration/index.md new file mode 100644 index 00000000..c07f3992 --- /dev/null +++ b/docs/continuous-integration/index.md @@ -0,0 +1,57 @@ +--- +title: Continuous Integration +--- + +Every push and pull request runs the checks below. They fall into three groups: what proves the +library works, what proves it can be consumed, and what builds the containers the rest run in. + +--- + +## Proving the library works {style: "api"} + +* `build` — compiles and runs the tests across the compiler matrix: gcc 12 to 16 and clang 16 to + 22, in Debug and Release, with clang additionally against libstdc++. A C++23 lane builds C++20 + as well, so the compilers it covers skip their standalone C++20 job rather than build it twice. +* `pre-commit` — runs the hooks of `.pre-commit-config.yaml`, `clang-format` among them, over the + whole tree rather than the diff. +* `codecov` — builds an instrumented tree, reports coverage to + [codecov.io](https://app.codecov.io/gh/libfn/functional). +* `sonarcloud` — builds with the compilation database and reports analysis to + [SonarCloud](https://sonarcloud.io/summary/new_code?id=libfn_functional). +* `licence` — scans dependencies with [FOSSA](https://app.fossa.com/projects/git%2Bgithub.com%2Flibfn%2Ffunctional). +* `docs` — builds this site, and publishes it only from `main`; a pull request builds it as a + check, so a broken reference fails review rather than the deployment. + +## Proving the library can be consumed {style: "api"} + +Each of these takes the library the way a user would, rather than building it in place: +`package-test-conan`, `package-test-vcpkg`, `package-test-nix` and `package-test-bazel`. Between +them they cover every packaging route the project offers, which is what keeps the packaging +metadata honest as the headers move. + +## Building the containers {style: "api"} + +`ci-build`, `ci-pre-commit` and `ci-docs` build the images the other workflows run in, from the +definitions in `ci/gcc`, `ci/clang`, `ci/pre-commit` and `ci/docs`. They run on the 11th and 26th +of each month, whenever an image definition changes on `main`, and on request. Each image is built +for amd64 and arm64 and published to `libfn.azurecr.io` under a tag naming the commit it was built +from; the workflows that consume an image name that exact tag, so a rebuild never changes what a +build runs against until the pin is moved deliberately. + +--- + +## Analysis of a pull request {style: "api"} + +Coverage and static analysis need credentials to report, and a pull request from a fork is not +given them — so `codecov` and `sonarcloud` each come in two parts. The workflow triggered by the +pull request builds without secrets and uploads its results as an artifact; a second workflow, +`codecov-pr-scan` or `sonarcloud-pr-scan`, wakes on that run completing, and reports from the +base repository where the credentials live. + +The two parts must agree about what was measured. The build is made at the pull request's head +commit rather than the merge commit GitHub offers by default, because the head is what the report +is attributed to; the artifact records that commit, and the scan refuses an artifact whose commit +is not the head it is reporting on — which is what a stale artifact looks like. + +Setting a fork up to report against your own Codecov and SonarCloud accounts is described in +[CONTRIBUTING](contributing/index). diff --git a/docs/copack/index.md b/docs/copack/index.md deleted file mode 100644 index 84d71908..00000000 --- a/docs/copack/index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Copack ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::copack - -:include-doxygen-doc: fn::copack_for - -## Apply {style: "api"} -Elimination: the active alternative routes into the callable, exhaustively - every alternative -must have a viable arm. - -:include-doxygen-member: fn::copack< Ts... >::apply { signatureOnly: false, includeAllMatches: true } - -## Transform {style: "api"} -The self-flattening map over the alternatives: the branch results form a new normalized copack. - -:include-doxygen-member: fn::copack< Ts... >::transform { signatureOnly: false, includeAllMatches: true } diff --git a/docs/coverage-exemptions.txt b/docs/coverage-exemptions.txt new file mode 100644 index 00000000..17f4b4aa --- /dev/null +++ b/docs/coverage-exemptions.txt @@ -0,0 +1,26 @@ +# Entities documented in the headers which no page is expected to name. +# +# scripts/check_docs_coverage.py fails on any other documented entity the site does not +# reach, so this file is where a deliberate omission is recorded - with the reason, so a +# later reader can judge whether it still holds. An entry that stops being needed is +# reported too, and should be deleted. +# +# Format: : + +fn::and_then_t::apply::operator(): the verb's dispatch object, an implementation detail of how the verb reaches a carrier's member; the verb itself is documented +fn::fail_t::apply::operator(): as above +fn::filter_t::apply::operator(): as above +fn::inspect_error_t::apply::operator(): as above +fn::inspect_t::apply::operator(): as above +fn::or_else_t::apply::operator(): as above +fn::recover_t::apply::operator(): as above +fn::transform_error_t::apply::operator(): as above +fn::transform_t::apply::operator(): as above +fn::value_or_t::apply::operator(): as above + +fn::expected< void, Err >::copack_value: void specialization artifact, cannot have a copack of void +fn::just< void >::emplace: void specialization artifact, cannot emplace void +fn::just< void >::v_: void specialization artifact, has no payload +fn::just< void >::~just: void specialization artifact, handled by primary template +fn::optional< T & >::const_iterator: reference specialization artifact, only has iterator +fn::optional< T & >::copack_value: reference specialization artifact, cannot copack a reference diff --git a/docs/expected/and_then.md b/docs/expected/and_then.md deleted file mode 100644 index 1923c0b1..00000000 --- a/docs/expected/and_then.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: fn::and_then ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::and_then_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::and_then_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-expected-and_then-value"], - desc: "The resulting value is `13` because `ex` does not contain an `Error` and therefore `and_then` is called." -} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-expected-and_then-error"], - desc: "The result is an `Error` because `ex` already contained an `Error` and therefore `and_then` is not called." -} diff --git a/docs/expected/discard.md b/docs/expected/discard.md deleted file mode 100644 index 20a2709c..00000000 --- a/docs/expected/discard.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: fn::discard ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::discard_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::discard_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -void - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-expected-discard"], - desc: "`42` is observed by `inspect` and the value is discarded by `discard` (no warning for discarded result of `inspect`)." -} diff --git a/docs/expected/fail.md b/docs/expected/fail.md deleted file mode 100644 index b03c92c5..00000000 --- a/docs/expected/fail.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::fail ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::fail_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::fail_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/filter.md b/docs/expected/filter.md deleted file mode 100644 index ec6302a7..00000000 --- a/docs/expected/filter.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: fn::filter ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::filter_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::filter_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-expected-filter-value"], - desc: "The resulting value is `42` because the filter predicate returns `true` for `42` as it is not less than `42`." -} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-expected-filter-error"], - desc: "The error is set to `Less than 42` because the predicate returns `false` for `12` since it's less than `42`." -} diff --git a/docs/expected/index.md b/docs/expected/index.md deleted file mode 100644 index 8fff1cb7..00000000 --- a/docs/expected/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Expected monad ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::expected - -## expected_unit {style: "api"} -The graded gateway: initiating a pipeline with this unit trigger opts all subsequent `and_then` -steps into graded error-set unioning, with no fake starting errors. - -:include-doxygen-member: fn::expected_unit { signatureOnly: false, includeAllMatches: true } - -## copack_error {style: "api"} -The explicit lift into the graded world, on the error side. - -:include-doxygen-member: fn::expected::copack_error { signatureOnly: false, includeAllMatches: true } - -## copack_value {style: "api"} -The same lift, on the value side. - -:include-doxygen-member: fn::expected::copack_value { signatureOnly: false, includeAllMatches: true } diff --git a/docs/expected/inspect.md b/docs/expected/inspect.md deleted file mode 100644 index 5bea1991..00000000 --- a/docs/expected/inspect.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::inspect ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::inspect_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::inspect_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/inspect_error.md b/docs/expected/inspect_error.md deleted file mode 100644 index 253c60da..00000000 --- a/docs/expected/inspect_error.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::inspect_error ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::inspect_error_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::inspect_error_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/or_else.md b/docs/expected/or_else.md deleted file mode 100644 index a9c48257..00000000 --- a/docs/expected/or_else.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::or_else ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::or_else_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::or_else_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/recover.md b/docs/expected/recover.md deleted file mode 100644 index aa1407b5..00000000 --- a/docs/expected/recover.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::recover ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::recover_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::recover_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/transform.md b/docs/expected/transform.md deleted file mode 100644 index 5dcc9e8d..00000000 --- a/docs/expected/transform.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::transform ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::transform_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::transform_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/transform_error.md b/docs/expected/transform_error.md deleted file mode 100644 index 084ed0d8..00000000 --- a/docs/expected/transform_error.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::transform_error ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::transform_error_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::transform_error_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/expected/value_or.md b/docs/expected/value_or.md deleted file mode 100644 index 53bfd5e5..00000000 --- a/docs/expected/value_or.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: fn::value_or ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::value_or_t - ---- - -## Return value {style: "api"} -The value of the monadic type if present; otherwise the user-provided fallback value. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 8bf56bf4..00000000 --- a/docs/index.md +++ /dev/null @@ -1,35 +0,0 @@ -# functional - -Functional programming in C++ - -## Why - -The purpose of this library is to exercise an approach to functional programming in C++ on top of the existing standard vocabulary types (such as `std::expected` and `std::optional`), with the aim of eventually extending future revisions of the C++ standard library with the functionality found to work well. - -## How - -The library comes as two parts: `pfn` (namespace `pfn`) is a faithful polyfill of the standard vocabulary types as specified for C++26, available to a C++20 compiler; `fn` (namespace `fn`) extends them with the facilities useful in writing functional style programs and adds new vocabulary types. Every `fn` type with a `pfn` counterpart is a strict superset of it: switching a valid program from `pfn` types to `fn` changes neither compilation nor behaviour. - -## What - -The library provides the following utilities: - -* functors - extensible system of encapsulation of monadic operations, expressed with a pipe `operator |` -* copack - coproduct of types (a sum of types), similar to `std::variant` but indexed by type rather than order, composes with the product of types -* choice monad - monad built on top of the coproduct of types, dispatch by overloading rules -* pack - product of types, similar to `std::tuple`, composes with the coproduct of types -* composition - monadic computations combined side by side: conjunction with `operator &`, disjunction with `operator |`, and their n-ary folds `fn::conjoin` and `fn::disjoin` -* multidispatch - dispatch any valid combination of product(s) and coproduct(s) to a function, based on overloading rules -* graded monad - integrate coproduct into `optional` and `expected` monads, enables extensible `expected` error types -* ... and more - -## Acknowledgments - -* Gašper Ažman, for providing the inspiration in ["(Fun)ctional C++ and the M-word"][gasper-functional-presentation] -* Bartosz Milewski, for taking the time to explain [parametrised and graded monads][parametrised-and-graded-monads] and [effect systems][effect-systems] -* [Ripple][ripple], for allowing the main author the time to work on this library - -[gasper-functional-presentation]: https://youtu.be/Jhggz8rtHbk?si=T-3DXPcvgE_Y5cpH -[parametrised-and-graded-monads]: https://arxiv.org/pdf/2001.10274.pdf -[effect-systems]: https://www.doc.ic.ac.uk/~dorchard/publ/haskell14-effects.pdf -[ripple]: https://ripple.com/ diff --git a/docs/just/index.md b/docs/just/index.md deleted file mode 100644 index e7a6b197..00000000 --- a/docs/just/index.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Just ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::just - -## value {style: "api"} -The payload, always present: the access is total, never throwing. - -:include-doxygen-member: fn::just::value { signatureOnly: false, includeAllMatches: true } - -## transform {style: "api"} -:include-doxygen-member: fn::just::transform { signatureOnly: false, includeAllMatches: true } - -## and_then {style: "api"} -:include-doxygen-member: fn::just::and_then { signatureOnly: false, includeAllMatches: true } diff --git a/docs/meta.json b/docs/meta.json index 671cc1c7..bf6d0f18 100644 --- a/docs/meta.json +++ b/docs/meta.json @@ -6,7 +6,7 @@ "title": "Support" }, "viewOn": { - "link": "https://github.com/libfn/functional/docs", + "link": "https://github.com/libfn/functional/blob/main", "title": "View On GitHub" } } diff --git a/docs/multidispatch/index.md b/docs/multidispatch/index.md deleted file mode 100644 index 3ac466c7..00000000 --- a/docs/multidispatch/index.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: Multidispatch ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -Elimination of the algebraic structures: `fn::apply` unpacks products and dispatches over -alternatives by ordinary C++ overload resolution, and `fn::overload` fuses per-alternative -lambdas into one overload set. Dispatch is exhaustive: an alternative without a viable arm makes -the whole call not applicable. - -## apply {style: "api"} -:include-doxygen-member: fn::apply { signatureOnly: false, includeAllMatches: true } - -## apply_r {style: "api"} -:include-doxygen-member: fn::apply_r { signatureOnly: false, includeAllMatches: true } - -## overload {style: "api"} - -##### Defined in {style: "api", badge: "#include "} - -:include-doxygen-doc: fn::overload diff --git a/docs/optional/and_then.md b/docs/optional/and_then.md deleted file mode 100644 index 0473e522..00000000 --- a/docs/optional/and_then.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: fn::and_then ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::and_then_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::and_then_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-optional-and_then-value"], - desc: "The resulting value is `13` because `op` is not a `nullopt` and therefore `and_then` is called." -} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-optional-and_then-empty"], - desc: "The result is a `nullopt` because `op` was already a `nullopt` and therefore `and_then` is not called." -} diff --git a/docs/optional/discard.md b/docs/optional/discard.md deleted file mode 100644 index 041282c3..00000000 --- a/docs/optional/discard.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: fn::discard ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::discard_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::discard_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -void - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-error-struct", "// example-optional-discard"], - desc: "`42` is observed by `inspect` and the value is discarded by `discard` (no warning for discarded result of `inspect`)." -} diff --git a/docs/optional/fail.md b/docs/optional/fail.md deleted file mode 100644 index b03c92c5..00000000 --- a/docs/optional/fail.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::fail ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::fail_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::fail_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/filter.md b/docs/optional/filter.md deleted file mode 100644 index 2ab225eb..00000000 --- a/docs/optional/filter.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: fn::filter ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::filter_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::filter_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. - ---- - -## Examples {style: "api"} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-optional-filter-value"], - desc: "The resulting value is `42` because the filter predicate returns `true` for `42` as it is not less than `42`." -} - -:include-template: templates/snippet.md { - path: "simple/main.cpp", - surroundedBy: ["// example-optional-filter-empty"], - desc: "The optional is empty because the predicate returns `false` for `12` since it's less than `42`." -} diff --git a/docs/optional/index.md b/docs/optional/index.md deleted file mode 100644 index 8b9f4fd7..00000000 --- a/docs/optional/index.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Optional monad ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::optional - -## copack_value {style: "api"} -The explicit lift into the graded world. - -:include-doxygen-member: fn::optional::copack_value { signatureOnly: false, includeAllMatches: true } diff --git a/docs/optional/inspect.md b/docs/optional/inspect.md deleted file mode 100644 index 5bea1991..00000000 --- a/docs/optional/inspect.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::inspect ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::inspect_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::inspect_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/inspect_error.md b/docs/optional/inspect_error.md deleted file mode 100644 index 253c60da..00000000 --- a/docs/optional/inspect_error.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::inspect_error ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::inspect_error_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::inspect_error_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/or_else.md b/docs/optional/or_else.md deleted file mode 100644 index a9c48257..00000000 --- a/docs/optional/or_else.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::or_else ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::or_else_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::or_else_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/recover.md b/docs/optional/recover.md deleted file mode 100644 index aa1407b5..00000000 --- a/docs/optional/recover.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::recover ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::recover_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::recover_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/transform.md b/docs/optional/transform.md deleted file mode 100644 index 5dcc9e8d..00000000 --- a/docs/optional/transform.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: fn::transform ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::transform_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::transform_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -A monadic type of the same kind. diff --git a/docs/optional/transform_error.md b/docs/optional/transform_error.md deleted file mode 100644 index 69d7822b..00000000 --- a/docs/optional/transform_error.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: fn::transform_error ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::transform_error_t - ---- - -## Call signatures {style: "api"} -:include-doxygen-member: fn::transform_error_t::operator() { signatureOnly: false, includeAllMatches: true } - ---- - -## Return value {style: "api"} -Not applicable: `transform_error` is rejected on `optional`, which has no error value to map. -Use `or_else` to act on the empty state instead. diff --git a/docs/optional/value_or.md b/docs/optional/value_or.md deleted file mode 100644 index 53bfd5e5..00000000 --- a/docs/optional/value_or.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: fn::value_or ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::value_or_t - ---- - -## Return value {style: "api"} -The value of the monadic type if present; otherwise the user-provided fallback value. diff --git a/docs/pack/index.md b/docs/pack/index.md deleted file mode 100644 index 8af126c3..00000000 --- a/docs/pack/index.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Packs ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: fn::pack - -## Append {style: "api"} -Grows the product without nesting: appending a value adds one field, and appending a pack -splices its fields in. -:include-doxygen-member: fn::pack::append { signatureOnly: false, includeAllMatches: true } - -## Apply {style: "api"} -Elimination: the elements spread into a callable as separate arguments. -:include-doxygen-member: fn::pack::apply { signatureOnly: false, includeAllMatches: true } diff --git a/docs/pfn/expected.md b/docs/pfn/expected.md deleted file mode 100644 index 0ed89322..00000000 --- a/docs/pfn/expected.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Expected polyfill ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: pfn::expected - -## expected over void {style: "api"} -The partial specialization serving computations which succeed with no value. - -:include-doxygen-doc: pfn::expected< void, E > - -## unexpected {style: "api"} - -:include-doxygen-doc: pfn::unexpected - -## unexpect {style: "api"} - -:include-doxygen-doc: pfn::unexpect_t - -## bad_expected_access {style: "api"} - -:include-doxygen-doc: pfn::bad_expected_access - -:include-doxygen-doc: pfn::bad_expected_access< void > diff --git a/docs/pfn/index.md b/docs/pfn/index.md deleted file mode 100644 index b568d009..00000000 --- a/docs/pfn/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Polyfill layer ---- - -The library is layered: namespace `pfn` is a faithful polyfill of standard vocabulary types and -utilities as specified for C++26, available to a C++20 compiler, and namespace `fn` builds the -functional-programming extensions on top of it. Every `fn` type with a `pfn` counterpart is a -strict superset of it: a valid program switching from `pfn` to `fn` changes neither compilation -nor behaviour. - -`pfn` polyfills only what C++20 lacks: all of ``, the C++23 and C++26 additions to -`std::optional` (the monadic operations, iterator support, `optional`), `std::apply` in its -SFINAE-friendly C++26 shape together with its applicability traits, `std::invoke_r` and -`std::unreachable`. Names C++20 already has — `std::nullopt`, `std::in_place`, -`std::bad_optional_access` — are used directly and not mirrored. - -The polyfills track the C++ working draft, deviating deliberately in three ways, each noted on -the entity it concerns: - -* where the standard leaves a member's `noexcept` specification unstated, one is derived from - the underlying types; every such clause is marked `// extension` in the source -* the draft's hardened preconditions are checked with an assertion, customizable by defining - `LIBFN_ASSERT` before inclusion -* `expected`'s comparison against a value is declared at namespace scope rather than as a - hidden friend, keeping its constraint deducible diff --git a/docs/pfn/optional.md b/docs/pfn/optional.md deleted file mode 100644 index 5c90b7b3..00000000 --- a/docs/pfn/optional.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Optional polyfill ---- - -##### Defined in {style: "api", badge: "#include "} - ---- - -:include-doxygen-doc: pfn::optional - -## optional over a reference {style: "api"} - -:include-doxygen-doc: pfn::optional< T & > diff --git a/docs/pfn/utility.md b/docs/pfn/utility.md deleted file mode 100644 index c4a095ef..00000000 --- a/docs/pfn/utility.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Utilities ---- - -## apply {style: "api"} - -##### Defined in {style: "api", badge: "#include "} - -:include-doxygen-member: pfn::apply { signatureOnly: false, includeAllMatches: true } - -### Applicability traits {style: "api"} -The C++26 traits `apply` is specified through; each also comes in its `_v` (for the two -predicates) or `_t` (for the result) form. - -:include-doxygen-doc: pfn::is_applicable - -:include-doxygen-doc: pfn::is_nothrow_applicable - -:include-doxygen-doc: pfn::apply_result - -## invoke_r {style: "api"} - -##### Defined in {style: "api", badge: "#include "} - -:include-doxygen-member: pfn::invoke_r { signatureOnly: false, includeAllMatches: true } - -## unreachable {style: "api"} - -##### Defined in {style: "api", badge: "#include "} - -:include-doxygen-member: pfn::unreachable { signatureOnly: false, includeAllMatches: true } diff --git a/docs/reference/and_then.md b/docs/reference/and_then.md new file mode 100644 index 00000000..ec2c5e9c --- /dev/null +++ b/docs/reference/and_then.md @@ -0,0 +1,68 @@ +--- +title: "functor fn::and_then" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::and_then_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::and_then"} +and_then_t and_then = {}; // (1) +``` + +:include-doxygen-doc: fn::and_then { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::and_then_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::and_then_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::and_then_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. + +--- + +## Examples {style: "api"} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-expected-and_then-value"], + desc: "The resulting value is `13` because `ex` does not contain an `Error` and therefore `and_then` is called." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-expected-and_then-error"], + desc: "The result is an `Error` because `ex` already contained an `Error` and therefore `and_then` is not called." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-optional-and_then-value"], + desc: "The resulting value is `13` because `op` is not a `nullopt` and therefore `and_then` is called." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-optional-and_then-empty"], + desc: "The result is a `nullopt` because `op` was already a `nullopt` and therefore `and_then` is not called." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-choice-parse", "// example-choice-checks"] +} diff --git a/docs/reference/apply.md b/docs/reference/apply.md new file mode 100644 index 00000000..1e4b41bf --- /dev/null +++ b/docs/reference/apply.md @@ -0,0 +1,83 @@ +--- +title: "multidispatch fn::apply" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +Elimination of the algebraic structures: `fn::apply` unpacks products and dispatches over +alternatives by ordinary C++ overload resolution, and `fn::overload` fuses per-alternative +lambdas into one overload set. Dispatch is exhaustive: an alternative without a viable arm makes +the whole call not applicable. + +## apply {style: "api"} + +```cpp {title: "fn::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) -> apply_result_t; // (1) +``` + +:include-doxygen-doc: fn::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::apply { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) -> Ret; // (1) +``` + +:include-doxygen-doc: fn::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + +## overload {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +:include-doxygen-doc: fn::overload + +## Applicability traits {style: "api"} + +```cpp {title: "fn::apply_result_t"} +template +using apply_result_t = typename apply_result::type; // (1) +``` + +:include-doxygen-doc: fn::apply_result_t { args: "" } + +```cpp {title: "fn::is_applicable_v"} +template +constexpr bool is_applicable_v = is_applicable::value; // (1) +``` + +:include-doxygen-doc: fn::is_applicable_v { args: "" } + +```cpp {title: "fn::is_applicable_r_v"} +template +constexpr bool is_applicable_r_v = is_applicable_r::value; // (1) +``` + +:include-doxygen-doc: fn::is_applicable_r_v { args: "" } + +```cpp {title: "fn::is_nothrow_applicable_v"} +template +constexpr bool is_nothrow_applicable_v = is_nothrow_applicable::value; // (1) +``` + +:include-doxygen-doc: fn::is_nothrow_applicable_v { args: "" } + +```cpp {title: "fn::is_nothrow_applicable_r_v"} +template +constexpr bool is_nothrow_applicable_r_v = is_nothrow_applicable_r::value; // (1) +``` + +:include-doxygen-doc: fn::is_nothrow_applicable_r_v { args: "" } + +:include-doxygen-doc: fn::is_applicable_r + +:include-doxygen-doc: fn::is_nothrow_applicable_r diff --git a/docs/reference/choice.md b/docs/reference/choice.md new file mode 100644 index 00000000..def99cdf --- /dev/null +++ b/docs/reference/choice.md @@ -0,0 +1,239 @@ +--- +title: "monad fn::choice" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::choice< Ts... > + +## Member types {style: "api"} + +```cpp {title: "fn::choice< Ts... >::value_type"} +using value_type = _impl; // (1) +``` + +:include-doxygen-doc: fn::choice< Ts... >::value_type { args: "" } + +```cpp {title: "fn::choice< Ts... >::select_nth"} +template +using select_nth = detail::select_nth_t; // (1) +``` + +:include-doxygen-doc: fn::choice< Ts... >::select_nth { args: "" } + +```cpp {title: "fn::choice< Ts... >::size"} +static std::size_t size = sizeof...(Ts); // (1) +``` + +:include-doxygen-doc: fn::choice< Ts... >::size { args: "" } + +```cpp {title: "fn::choice< Ts... >::has_type"} +template +static constexpr bool has_type = _impl::template has_type; // (1) +``` + +:include-doxygen-doc: fn::choice< Ts... >::has_type { args: "" } + +## Construction {style: "api"} + +From a value of one alternative, in place from arguments, or widening from a `copack` over a +subset of the alternatives. + +```cpp {title: "fn::choice< Ts... >::choice"} +template +constexpr choice(T &&v); // (1) +constexpr explicit choice(T &&v); // (2) +constexpr explicit choice(std::in_place_type_t d, auto &&...args); // (3) + +template +constexpr choice(copack const &v); // (4) +constexpr choice(copack &&v); // (5) +constexpr choice(std::in_place_type_t>, some_copack auto &&v); // (6) + +constexpr choice(choice const &other) = default; // (7) +constexpr choice(choice &&other) = default; // (8) +``` + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "T &&" } + +:include-doxygen-doc-params: fn::choice< Ts... >::choice { args: "T &&", title: "parameters" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "::std::in_place_type_t< T >, auto &&..." } + +:include-doxygen-doc-params: fn::choice< Ts... >::choice { args: "::std::in_place_type_t< T >, auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::choice< Ts... >::choice { args: "::std::in_place_type_t< T >, auto &&...", title: "parameters" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "copack < Tx... > const &" } + +:include-doxygen-doc-params: fn::choice< Ts... >::choice { args: "copack < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "copack < Tx... > &&" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "::std::in_place_type_t< copack < Tx... > >, some_copack auto &&" } + +:include-doxygen-doc-params: fn::choice< Ts... >::choice { args: "::std::in_place_type_t< copack < Tx... > >, some_copack auto &&", title: "parameters" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "choice const &" } + +:include-doxygen-doc: fn::choice< Ts... >::choice { args: "choice &&" } + +## Destructor {style: "api"} + +```cpp {title: "fn::choice< Ts... >::~choice"} +constexpr ~choice() = default; // (1) +``` + +:include-doxygen-doc: fn::choice< Ts... >::~choice { args: "" } + +## Assignment {style: "api"} + +```cpp {title: "fn::choice< Ts... >::operator="} +constexpr auto operator=(choice const &other) = default -> choice &; // (1) +constexpr auto operator=(choice &&other) = default -> choice &; // (2) + +template +constexpr auto operator=(copack const &arg) -> choice &; // (3) +constexpr auto operator=(copack &&arg) -> choice &; // (4) + +template +constexpr auto operator=(U &&v) -> choice &; // (5) +``` + +:include-doxygen-doc: fn::choice< Ts... >::operator= { args: "choice const &" } + +:include-doxygen-doc: fn::choice< Ts... >::operator= { args: "choice &&" } + +:include-doxygen-doc: fn::choice< Ts... >::operator= { args: "copack < Tx... > const &" } + +:include-doxygen-doc: fn::choice< Ts... >::operator= { args: "copack < Tx... > &&" } + +:include-doxygen-doc: fn::choice< Ts... >::operator= { args: "U &&" } + +## choice_for {style: "api"} + +The construction alias: accepts alternatives in any order, with duplicates and nested copacks, +and resolves to the canonical `fn::choice`. Prefer it over spelling `choice` directly, so that no +spelling in your project is tied to one compiler's alternative order. + +```cpp {title: "fn::choice_for"} +template +using choice_for = detail::_collapsing_copack::normalized<::fn::choice, detail::_collapsing_copack::flattened>::type; // (1) +``` + +:include-doxygen-doc: fn::choice_for { args: "" } + +:include-doxygen-doc-params: fn::choice_for { args: "", type: "template", title: "template parameters" } + +## Deduction guides {style: "api"} + +```cpp {title: "fn::choice"} +template +choice(std::in_place_type_t, auto &&...) -> choice; // (1) +choice(T) -> choice; // (2) +``` + +## value {style: "api"} + +The alternatives as the underlying `copack`, always present. + +```cpp {title: "fn::choice< Ts... >::value"} +constexpr auto value() & -> value_type &; // (1) +constexpr auto value() const & -> value_type const &; // (2) +constexpr auto value() && -> value_type &&; // (3) +constexpr auto value() const && -> value_type const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::value { args: "" } + +:include-doxygen-doc-params: fn::choice< Ts... >::value { args: "", title: "parameters" } + +## and_then {style: "api"} + +```cpp {title: "fn::choice< Ts... >::and_then"} +template +constexpr auto and_then(Fn &&fn) &; // (1) +constexpr auto and_then(Fn &&fn) const &; // (2) +constexpr auto and_then(Fn &&fn) &&; // (3) +constexpr auto and_then(Fn &&fn) const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::and_then { args: "Fn &&" } + +:include-doxygen-doc-params: fn::choice< Ts... >::and_then { args: "Fn &&", title: "parameters" } + +## transform {style: "api"} + +```cpp {title: "fn::choice< Ts... >::transform"} +template +constexpr auto transform(Fn &&fn) &; // (1) +constexpr auto transform(Fn &&fn) const &; // (2) +constexpr auto transform(Fn &&fn) &&; // (3) +constexpr auto transform(Fn &&fn) const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::transform { args: "Fn &&" } + +:include-doxygen-doc-params: fn::choice< Ts... >::transform { args: "Fn &&", title: "parameters" } + +## apply {style: "api"} + +```cpp {title: "fn::choice< Ts... >::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) &; // (1) +constexpr auto apply(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto apply(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto apply(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::choice< Ts... >::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) &; // (1) +constexpr auto apply_r(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto apply_r(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto apply_r(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type {style: "api"} + +```cpp {title: "fn::choice< Ts... >::apply_type"} +template +constexpr auto apply_type(Fn &&fn, Args &&...args) &; // (1) +constexpr auto apply_type(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto apply_type(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto apply_type(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::apply_type { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply_type { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type_r {style: "api"} + +```cpp {title: "fn::choice< Ts... >::apply_type_r"} +template +constexpr auto apply_type_r(Fn &&fn, Args &&...args) & -> Ret; // (1) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const & -> Ret; // (2) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) && -> Ret; // (3) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::choice< Ts... >::apply_type_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::choice< Ts... >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } diff --git a/docs/reference/comparison.md b/docs/reference/comparison.md new file mode 100644 index 00000000..c157f8ff --- /dev/null +++ b/docs/reference/comparison.md @@ -0,0 +1,166 @@ +--- +title: "other fn comparisons" +--- + +Each carrier is compared where its contents are, and against the states it can be in. These are +declared alongside the carrier they serve rather than in one header, so reach them by including +the carrier's own. + +An empty or failed operand equals nothing and orders before every value, so a comparison answers +rather than throwing; where a contained type has no such operator the comparison is simply not +viable, not ill-formed. + +## operator== {style: "api"} + +```cpp {title: "fn::operator=="} +template +constexpr auto operator==(choice const &lh, choice const &rh) -> bool; // (1) +constexpr auto operator==(copack const &lh, copack const &rh) -> bool; // (2) + +template +constexpr auto operator==(expected const &x, T2 const &v) -> bool; // (3) + +template +constexpr auto operator==(just const &lh, just const &rh) -> bool; // (4) +constexpr auto operator==(just const &lh, U const &rh) -> bool; // (5) + +template +constexpr auto operator==(optional const &x, optional const &y) -> bool; // (6) + +template +constexpr auto operator==(optional const &x, std::nullopt_t) -> bool; // (7) + +template +constexpr auto operator==(optional const &x, U const &v) -> bool; // (8) +constexpr auto operator==(T const &v, optional const &x) -> bool; // (9) +``` + +:include-doxygen-doc: fn::operator== { args: "choice < Ts... > const &, choice < Tx... > const &" } + +:include-doxygen-doc-params: fn::operator== { args: "choice < Ts... > const &, choice < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::operator== { args: "copack < Ts... > const &, copack < Tx... > const &" } + +:include-doxygen-doc-params: fn::operator== { args: "copack < Ts... > const &, copack < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::operator== { args: "just < T > const &, just < U > const &" } + +:include-doxygen-doc: fn::operator== { args: "just < T > const &, U const &" } + +:include-doxygen-doc: fn::operator== { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator== { args: "optional < T > const &, ::std::nullopt_t" } + +:include-doxygen-doc: fn::operator== { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator== { args: "T const &, optional < U > const &" } + +## operator!= {style: "api"} + +```cpp {title: "fn::operator!="} +template +constexpr auto operator!=(choice const &lh, choice const &rh) -> bool; // (1) + +template +constexpr auto operator!=(optional const &x, optional const &y) -> bool; // (2) +constexpr auto operator!=(optional const &x, U const &v) -> bool; // (3) +constexpr auto operator!=(T const &v, optional const &x) -> bool; // (4) +``` + +:include-doxygen-doc: fn::operator!= { args: "choice < Ts... > const &, choice < Tx... > const &" } + +:include-doxygen-doc-params: fn::operator!= { args: "choice < Ts... > const &, choice < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::operator!= { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator!= { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator!= { args: "T const &, optional < U > const &" } + +## operator< {style: "api"} + +```cpp {title: "fn::operator<"} +template +constexpr auto operator<(optional const &x, optional const &y) -> bool; // (1) +constexpr auto operator<(optional const &x, U const &v) -> bool; // (2) +constexpr auto operator<(T const &v, optional const &x) -> bool; // (3) +``` + +:include-doxygen-doc: fn::operator< { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator< { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator< { args: "T const &, optional < U > const &" } + +## operator<= {style: "api"} + +```cpp {title: "fn::operator<="} +template +constexpr auto operator<=(optional const &x, optional const &y) -> bool; // (1) +constexpr auto operator<=(optional const &x, U const &v) -> bool; // (2) +constexpr auto operator<=(T const &v, optional const &x) -> bool; // (3) +``` + +:include-doxygen-doc: fn::operator<= { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator<= { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator<= { args: "T const &, optional < U > const &" } + +## operator> {style: "api"} + +```cpp {title: "fn::operator>"} +template +constexpr auto operator>(optional const &x, optional const &y) -> bool; // (1) +constexpr auto operator>(optional const &x, U const &v) -> bool; // (2) +constexpr auto operator>(T const &v, optional const &x) -> bool; // (3) +``` + +:include-doxygen-doc: fn::operator> { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator> { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator> { args: "T const &, optional < U > const &" } + +## operator>= {style: "api"} + +```cpp {title: "fn::operator>="} +template +constexpr auto operator>=(optional const &x, optional const &y) -> bool; // (1) +constexpr auto operator>=(optional const &x, U const &v) -> bool; // (2) +constexpr auto operator>=(T const &v, optional const &x) -> bool; // (3) +``` + +:include-doxygen-doc: fn::operator>= { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator>= { args: "optional < T > const &, U const &" } + +:include-doxygen-doc: fn::operator>= { args: "T const &, optional < U > const &" } + +## operator<=> {style: "api"} + +```cpp {title: "fn::operator<=>"} +template U> +auto operator<=>(optional const &x, optional const &y) -> std::compare_three_way_result_t; // (1) + +template +constexpr auto operator<=>(optional const &x, std::nullopt_t) -> std::strong_ordering; // (2) + +template +auto operator<=>(optional const &x, U const &v) -> std::compare_three_way_result_t; // (3) +``` + +:include-doxygen-doc: fn::operator<=> { args: "optional < T > const &, optional < U > const &" } + +:include-doxygen-doc: fn::operator<=> { args: "optional < T > const &, ::std::nullopt_t" } + +:include-doxygen-doc: fn::operator<=> { args: "optional < T > const &, U const &" } + +## swap {style: "api"} + +```cpp {title: "fn::swap"} +template +constexpr auto swap(optional &x, optional &y) -> void; // (1) +``` + +:include-doxygen-doc: fn::swap { args: "optional < T > &, optional < T > &" } diff --git a/docs/reference/concepts.md b/docs/reference/concepts.md new file mode 100644 index 00000000..f336ed99 --- /dev/null +++ b/docs/reference/concepts.md @@ -0,0 +1,155 @@ +--- +title: "other fn concepts" +--- + +The constraints the library states its own rules with, and which client code can state its +rules with too. They fall into four groups: what a type is, how two carriers relate, what a +value converts to, and whether an operation applies. + +Each is declared with the feature it constrains rather than gathered in one place, so every +concept below names the header that declares it. `` carries the general ones. + +--- + +## What a type is {style: "api"} + +### fn::some_monadic_type {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_monadic_type + +### fn::some_expected {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_expected + +### fn::some_expected_void {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_expected_void + +### fn::some_expected_non_void {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_expected_non_void + +### fn::some_optional {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_optional + +### fn::some_just {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_just + +### fn::some_choice {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_choice + +### fn::some_pack {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_pack + +### fn::some_copack {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_copack + +### fn::empty_copack {style: "api", badge: "#include "} +:include-doxygen-doc: fn::empty_copack + +### fn::some_identity {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_identity + +### fn::some_empty_error {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_empty_error + +### fn::some_empty_value {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_empty_value + +### fn::some_in_place_type {style: "api", badge: "#include "} +:include-doxygen-doc: fn::some_in_place_type + +--- + +## How two carriers relate {style: "api"} + +### fn::same_kind {style: "api", badge: "#include "} +:include-doxygen-doc: fn::same_kind + +### fn::same_value_kind {style: "api", badge: "#include "} +:include-doxygen-doc: fn::same_value_kind + +### fn::same_monadic_type_as {style: "api", badge: "#include "} +:include-doxygen-doc: fn::same_monadic_type_as + +--- + +## What a value converts to {style: "api"} + +### fn::convertible_to_expected {style: "api", badge: "#include "} +:include-doxygen-doc: fn::convertible_to_expected + +### fn::convertible_to_optional {style: "api", badge: "#include "} +:include-doxygen-doc: fn::convertible_to_optional + +### fn::convertible_to_choice {style: "api", badge: "#include "} +:include-doxygen-doc: fn::convertible_to_choice + +### fn::convertible_to_unexpected {style: "api", badge: "#include "} +:include-doxygen-doc: fn::convertible_to_unexpected + +### fn::convertible_to_bool {style: "api", badge: "#include "} +:include-doxygen-doc: fn::convertible_to_bool + +--- + +## Whether an operation applies {style: "api"} + +`monadic_invocable` is the constraint `operator|` itself carries; the rest are the per-verb +constraints it dispatches to, and the ones a verb of your own would join. + +### fn::monadic_invocable {style: "api", badge: "#include "} +:include-doxygen-doc: fn::monadic_invocable + +### fn::applicable_and_then {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_and_then + +### fn::applicable_and_then_across {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_and_then_across + +### fn::applicable_transform {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_transform + +### fn::applicable_transform_error {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_transform_error + +### fn::applicable_transform_promote {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_transform_promote + +### fn::applicable_or_else {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_or_else + +### fn::applicable_or_else_across {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_or_else_across + +### fn::applicable_recover {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_recover + +### fn::applicable_filter {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_filter + +### fn::applicable_inspect {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_inspect + +### fn::applicable_inspect_error {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_inspect_error + +### fn::applicable_fail {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_fail + +### fn::applicable_value_or {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable_value_or + +--- + +## Whether a callable applies {style: "api"} + +The constraints behind `fn::apply`, over plain arguments and over a type list alike. + +### fn::applicable {style: "api", badge: "#include "} +:include-doxygen-doc: fn::applicable + +### fn::regular_applicable {style: "api", badge: "#include "} +:include-doxygen-doc: fn::regular_applicable + +### fn::typelist_applicable {style: "api", badge: "#include "} +:include-doxygen-doc: fn::typelist_applicable + +### fn::typelist_applicable_r {style: "api", badge: "#include "} +:include-doxygen-doc: fn::typelist_applicable_r diff --git a/docs/reference/conjoin.md b/docs/reference/conjoin.md new file mode 100644 index 00000000..117086f1 --- /dev/null +++ b/docs/reference/conjoin.md @@ -0,0 +1,112 @@ +--- +title: "fold fn::conjoin" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +Independent computations compose side by side. The conjunction `a & b` keeps both results: values +multiply into a `pack` and errors sum into a `copack`, with the leftmost failing operand's error +held at runtime. Each carrier's header declares its own `&`; this header defines the n-ary fold +over it. + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::conjoin"} +conjoin_t conjoin; // (1) +``` + +:include-doxygen-doc: fn::conjoin { args: "" } + +## conjoin {style: "api"} + +:include-doxygen-doc: fn::conjoin_t + +--- + +## The operator {style: "api"} + +The binary conjunction each carrier declares; `conjoin` is its n-ary fold. + +```cpp {title: "fn::operator&"} +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (1) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (2) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (3) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected::value_type, typename std::remove_cvref_t::error_type>; // (4) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected::value_type, typename std::remove_cvref_t::error_type>; // (5) + +template +constexpr auto operator&(Lh &&, Rh &&rh); // (6) + +template +constexpr auto operator&(Lh &&lh, Rh &&); // (7) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (8) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (9) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (10) + +template +constexpr auto operator&(Lh &&, Rh &&rh); // (11) + +template +constexpr auto operator&(Lh &&lh, Rh &&); // (12) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (13) + +template +constexpr auto operator&(Lh &&lh, Rh &&rh); // (14) + +template +constexpr auto operator&(Lh &&, Rh &&rh); // (15) + +template +constexpr auto operator&(Lh &&lh, Rh &&); // (16) + +constexpr auto operator&(auto &&lh, auto &&rh); // (17) +``` + +:include-doxygen-doc: fn::operator& { args: "Lh &&, Rh &&" } + +:include-doxygen-doc-params: fn::operator& { args: "Lh &&, Rh &&", title: "parameters" } + +:include-doxygen-doc: fn::operator& { args: "auto &&, auto &&" } + +:include-doxygen-doc-params: fn::operator& { args: "auto &&, auto &&", title: "parameters" } + +--- + +## Call signatures {style: "api"} + +```cpp {title: "fn::conjoin_t::operator()"} +template +constexpr auto operator()(Arg &&arg) const -> decltype(arg); // (1) + +template +constexpr auto operator()(Arg &&arg, Args &&...args) const; // (2) +``` + +:include-doxygen-doc: fn::conjoin_t::operator() { args: "Arg &&" } + +:include-doxygen-doc-params: fn::conjoin_t::operator() { args: "Arg &&", title: "parameters" } + +:include-doxygen-doc: fn::conjoin_t::operator() { args: "Arg &&, Args &&..." } + +:include-doxygen-doc-params: fn::conjoin_t::operator() { args: "Arg &&, Args &&...", title: "parameters" } diff --git a/docs/reference/copack.md b/docs/reference/copack.md new file mode 100644 index 00000000..b8ce4584 --- /dev/null +++ b/docs/reference/copack.md @@ -0,0 +1,325 @@ +--- +title: "type fn::copack" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::copack + +:include-doxygen-doc: fn::copack_for + +## Member types {style: "api"} + +```cpp {title: "fn::copack< Ts... >::data_t"} +using data_t = detail::variadic_union; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::data_t { args: "" } + +```cpp {title: "fn::copack< Ts... >::data"} +data_t data; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::data { args: "" } + +```cpp {title: "fn::copack< Ts... >::index"} +std::size_t index; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::index { args: "" } + +```cpp {title: "fn::copack< Ts... >::size"} +static std::size_t size = sizeof...(Ts); // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::size { args: "" } + +```cpp {title: "fn::copack<>::size"} +static std::size_t size = 0; // (1) +``` + +:include-doxygen-doc: fn::copack<>::size { args: "" } + +```cpp {title: "fn::copack<>::has_type"} +template +static constexpr bool has_type = false; // (1) +``` + +:include-doxygen-doc: fn::copack<>::has_type { args: "" } + +```cpp {title: "fn::copack< Ts... >::has_type"} +template +static constexpr bool has_type = data_t::template has_type; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::has_type { args: "" } + +:include-doxygen-doc-params: fn::copack< Ts... >::has_type { args: "", type: "template", title: "template parameters" } + +```cpp {title: "fn::copack< Ts... >::select_nth"} +template +using select_nth = detail::select_nth_t; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::select_nth { args: "" } + +:include-doxygen-doc-params: fn::copack< Ts... >::select_nth { args: "", type: "template", title: "template parameters" } + +## Construction {style: "api"} + +```cpp {title: "fn::copack<>::copack"} +constexpr copack() noexcept = delete; // (1) +constexpr copack(copack const &) noexcept = default; // (2) +constexpr copack(copack &&) noexcept = default; // (3) +``` + +:include-doxygen-doc: fn::copack<>::copack { args: "" } + +:include-doxygen-doc: fn::copack<>::copack { args: "copack const &" } + +:include-doxygen-doc: fn::copack<>::copack { args: "copack &&" } + +```cpp {title: "fn::copack< Ts... >::copack"} +template +constexpr copack(T &&v); // (1) +constexpr explicit copack(T &&v); // (2) +constexpr explicit copack(std::in_place_type_t, auto &&...args); // (3) + +template +constexpr copack(copack const &arg); // (4) +constexpr copack(copack &&arg); // (5) +constexpr copack(std::in_place_type_t>, some_copack auto &&arg); // (6) + +constexpr copack(copack const &other) = default; // (7) +constexpr copack(copack const &other); // (8) +constexpr copack(copack &&other) = default; // (9) +constexpr copack(copack &&other); // (10) +``` + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "T &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "T &&", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "::std::in_place_type_t< T >, auto &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "::std::in_place_type_t< T >, auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "::std::in_place_type_t< T >, auto &&...", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "copack < Tx... > const &" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "copack < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "copack < Tx... > &&" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "::std::in_place_type_t< copack < Tx... > >, some_copack auto &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "::std::in_place_type_t< copack < Tx... > >, some_copack auto &&", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "copack const &" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "copack const &", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::copack { args: "copack &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::copack { args: "copack &&", title: "parameters" } + +## Destructor {style: "api"} + +```cpp {title: "fn::copack< Ts... >::~copack"} +constexpr ~copack() = default; // (1) +constexpr ~copack(); // (2) +``` + +:include-doxygen-doc: fn::copack< Ts... >::~copack { args: "" } + +```cpp {title: "fn::copack<>::~copack"} +constexpr ~copack() noexcept = default; // (1) +``` + +:include-doxygen-doc: fn::copack<>::~copack { args: "" } + +## emplace {style: "api"} + +```cpp {title: "fn::copack< Ts... >::emplace"} +template +constexpr auto emplace(auto &&...args) -> T &; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::emplace { args: "auto &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::emplace { args: "auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::emplace { args: "auto &&...", title: "parameters" } + +## Assignment {style: "api"} + +```cpp {title: "fn::copack<>::operator="} +constexpr auto operator=(copack const &) noexcept = default -> copack &; // (1) +constexpr auto operator=(copack &&) noexcept = default -> copack &; // (2) +``` + +:include-doxygen-doc: fn::copack<>::operator= { args: "copack const &" } + +:include-doxygen-doc: fn::copack<>::operator= { args: "copack &&" } + +```cpp {title: "fn::copack< Ts... >::operator="} +constexpr auto operator=(copack const &other) = default -> copack &; // (1) +constexpr auto operator=(copack const &other) -> copack &; // (2) +constexpr auto operator=(copack &&other) = default -> copack &; // (3) +constexpr auto operator=(copack &&other) -> copack &; // (4) + +template +constexpr auto operator=(copack const &arg) -> copack &; // (5) +constexpr auto operator=(copack &&arg) -> copack &; // (6) + +template +constexpr auto operator=(U &&v) -> copack &; // (7) +``` + +:include-doxygen-doc: fn::copack< Ts... >::operator= { args: "copack const &" } + +:include-doxygen-doc-params: fn::copack< Ts... >::operator= { args: "copack const &", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::operator= { args: "copack &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::operator= { args: "copack &&", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::operator= { args: "copack < Tx... > const &" } + +:include-doxygen-doc-params: fn::copack< Ts... >::operator= { args: "copack < Tx... > const &", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::operator= { args: "copack < Tx... > &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::operator= { args: "copack < Tx... > &&", title: "parameters" } + +:include-doxygen-doc: fn::copack< Ts... >::operator= { args: "U &&" } + +:include-doxygen-doc-params: fn::copack< Ts... >::operator= { args: "U &&", title: "parameters" } + +## Apply {style: "api"} + +Elimination: the active alternative routes into the callable, exhaustively - every alternative +must have a viable arm. + +```cpp {title: "fn::copack< Ts... >::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) &; // (1) +constexpr auto apply(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto apply(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto apply(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::copack< Ts... >::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply { args: "Fn &&, Args &&...", title: "parameters" } + +## Transform {style: "api"} + +The self-flattening map over the alternatives: the branch results form a new normalized copack. + +```cpp {title: "fn::copack< Ts... >::transform"} +template +constexpr auto transform(Fn &&fn, Args &&...args) &; // (1) +constexpr auto transform(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto transform(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto transform(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::copack< Ts... >::transform { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::transform { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::copack< Ts... >::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) & -> Ret; // (1) +constexpr auto apply_r(Fn &&fn, Args &&...args) const & -> Ret; // (2) +constexpr auto apply_r(Fn &&fn, Args &&...args) && -> Ret; // (3) +constexpr auto apply_r(Fn &&fn, Args &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::copack< Ts... >::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type {style: "api"} + +```cpp {title: "fn::copack< Ts... >::apply_type"} +template +constexpr auto apply_type(Fn &&fn, Args &&...args) &; // (1) +constexpr auto apply_type(Fn &&fn, Args &&...args) const &; // (2) +constexpr auto apply_type(Fn &&fn, Args &&...args) &&; // (3) +constexpr auto apply_type(Fn &&fn, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::copack< Ts... >::apply_type { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply_type { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type_r {style: "api"} + +```cpp {title: "fn::copack< Ts... >::apply_type_r"} +template +constexpr auto apply_type_r(Fn &&fn, Args &&...args) & -> Ret; // (1) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const & -> Ret; // (2) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) && -> Ret; // (3) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::copack< Ts... >::apply_type_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } + +## get_ptr {style: "api"} + +```cpp {title: "fn::copack< Ts... >::get_ptr"} +template +constexpr auto get_ptr(std::in_place_type_t=std::in_place_type) -> T *; // (1) +constexpr auto get_ptr(std::in_place_type_t=std::in_place_type) const -> T const *; // (2) +``` + +:include-doxygen-doc: fn::copack< Ts... >::get_ptr { args: "::std::in_place_type_t< T >" } + +:include-doxygen-doc-params: fn::copack< Ts... >::get_ptr { args: "::std::in_place_type_t< T >", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::get_ptr { args: "::std::in_place_type_t< T >", title: "parameters" } + +## has_value {style: "api"} + +```cpp {title: "fn::copack< Ts... >::has_value"} +template +constexpr auto has_value(std::in_place_type_t=std::in_place_type) const -> bool; // (1) +``` + +:include-doxygen-doc: fn::copack< Ts... >::has_value { args: "::std::in_place_type_t< T >" } + +:include-doxygen-doc-params: fn::copack< Ts... >::has_value { args: "::std::in_place_type_t< T >", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::copack< Ts... >::has_value { args: "::std::in_place_type_t< T >", title: "parameters" } + +## as_copack {style: "api"} + +```cpp {title: "fn::as_copack"} +constexpr auto as_copack(auto &&src) -> decltype(auto); // (1) + +template +constexpr auto as_copack(std::in_place_type_t, auto &&...args) -> decltype(auto); // (2) +``` + +:include-doxygen-doc: fn::as_copack { args: "auto &&" } + +:include-doxygen-doc-params: fn::as_copack { args: "auto &&", title: "parameters" } + +:include-doxygen-doc: fn::as_copack { args: "::std::in_place_type_t< T >, auto &&..." } + +:include-doxygen-doc-params: fn::as_copack { args: "::std::in_place_type_t< T >, auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::as_copack { args: "::std::in_place_type_t< T >, auto &&...", title: "parameters" } diff --git a/docs/reference/discard.md b/docs/reference/discard.md new file mode 100644 index 00000000..72fdfb6e --- /dev/null +++ b/docs/reference/discard.md @@ -0,0 +1,51 @@ +--- +title: "functor fn::discard" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::discard_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::discard"} +discard_t discard = {}; // (1) +``` + +:include-doxygen-doc: fn::discard { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::discard_t::operator()"} +constexpr auto operator()() const -> functor; // (1) +``` + +:include-doxygen-doc: fn::discard_t::operator() { args: "" } + +:include-doxygen-doc-params: fn::discard_t::operator() { args: "", title: "parameters" } + +--- + +## Return value {style: "api"} + +void + +--- + +## Examples {style: "api"} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-expected-discard"], + desc: "`42` is observed by `inspect` and the value is discarded by `discard` (no warning for discarded result of `inspect`)." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-optional-discard"], + desc: "`42` is observed by `inspect` and the value is discarded by `discard` (no warning for discarded result of `inspect`)." +} diff --git a/docs/reference/disjoin.md b/docs/reference/disjoin.md new file mode 100644 index 00000000..b3214881 --- /dev/null +++ b/docs/reference/disjoin.md @@ -0,0 +1,74 @@ +--- +title: "fold fn::disjoin" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +The disjunction `a | b` keeps the first operand that worked: values sum into a `copack` and errors +multiply into a `pack`, present only when every operand failed. Each carrier's header declares its +own `|`; this header defines the n-ary fold over it. This is the operator's disjunction meaning, +told apart by its right operand — a carrier. With a pipeline functor on the right it feeds the +carrier into that operation instead. + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::disjoin"} +disjoin_t disjoin; // (1) +``` + +:include-doxygen-doc: fn::disjoin { args: "" } + +## disjoin {style: "api"} + +:include-doxygen-doc: fn::disjoin_t + +--- + +## The operator {style: "api"} + +The binary disjunction each carrier declares; `disjoin` is its n-ary fold. + +```cpp {title: "fn::operator|"} +template +constexpr auto operator|(Lh &&lh, Rh &&rh); // (1) + +template +constexpr auto operator|(Lh &&lh, Rh &&rh); // (2) + +template +constexpr auto operator|(Lh &&lh, Rh &&rh); // (3) + +template +constexpr auto operator|(Lh &&lh, Rh &&rh); // (4) + +template +constexpr auto operator|(Lh &&lh, Rh &&rh); // (5) +``` + +:include-doxygen-doc: fn::operator| { args: "Lh &&, Rh &&" } + +:include-doxygen-doc-params: fn::operator| { args: "Lh &&, Rh &&", title: "parameters" } + +--- + +## Call signatures {style: "api"} + +```cpp {title: "fn::disjoin_t::operator()"} +template +constexpr auto operator()(Arg &&arg) const -> decltype(arg); // (1) + +template +constexpr auto operator()(Arg &&arg, Args &&...args) const; // (2) +``` + +:include-doxygen-doc: fn::disjoin_t::operator() { args: "Arg &&" } + +:include-doxygen-doc-params: fn::disjoin_t::operator() { args: "Arg &&", title: "parameters" } + +:include-doxygen-doc: fn::disjoin_t::operator() { args: "Arg &&, Args &&..." } + +:include-doxygen-doc-params: fn::disjoin_t::operator() { args: "Arg &&, Args &&...", title: "parameters" } diff --git a/docs/reference/expected.md b/docs/reference/expected.md new file mode 100644 index 00000000..37517ba7 --- /dev/null +++ b/docs/reference/expected.md @@ -0,0 +1,532 @@ +--- +title: "monad fn::expected" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::expected + +## Member types {style: "api"} + +```cpp {title: "fn::expected::value_type"} +using value_type = T; // (1) +``` + +:include-doxygen-doc: fn::expected::value_type { args: "" } + +```cpp {title: "fn::expected::error_type"} +using error_type = Err; // (1) +``` + +:include-doxygen-doc: fn::expected::error_type { args: "" } + +```cpp {title: "fn::expected::unexpected_type"} +using unexpected_type = ::fn::unexpected; // (1) +``` + +:include-doxygen-doc: fn::expected::unexpected_type { args: "" } + +```cpp {title: "fn::expected::rebind"} +template +using rebind = expected; // (1) +``` + +:include-doxygen-doc: fn::expected::rebind { args: "" } + +```cpp {title: "fn::expected< void, Err >::value_type"} +using value_type = void; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::value_type { args: "" } + +```cpp {title: "fn::expected< void, Err >::error_type"} +using error_type = Err; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::error_type { args: "" } + +```cpp {title: "fn::expected< void, Err >::unexpected_type"} +using unexpected_type = ::fn::unexpected; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::unexpected_type { args: "" } + +```cpp {title: "fn::expected< void, Err >::rebind"} +template +using rebind = expected; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::rebind { args: "" } + +## Construction {style: "api"} + +```cpp {title: "fn::expected::expected"} +constexpr expected(); // (1) + +template +constexpr explicit expected(expected const &s); // (2) +constexpr explicit expected(expected &&s); // (3) + +template +constexpr explicit expected(U &&v); // (4) + +template +constexpr explicit expected(::fn::unexpected const &g); // (5) +constexpr explicit expected(::fn::unexpected &&g); // (6) + +template +constexpr explicit expected(std::in_place_t, Args &&...a); // (7) + +template +constexpr explicit expected(std::in_place_t, std::initializer_list il, Args &&...a); // (8) + +template +constexpr explicit expected(::fn::unexpect_t, Args &&...a); // (9) + +template +constexpr explicit expected(::fn::unexpect_t, std::initializer_list il, Args &&...a); // (10) + +constexpr expected(expected const &) = delete; // (11) +constexpr expected(expected const &s) = default; // (12) +constexpr expected(expected const &s); // (13) +constexpr expected(expected &&s) noexcept = default; // (14) +constexpr expected(expected &&s); // (15) + +template +constexpr explicit expected(::pfn::detail::_expected_from_invoke_t tag, Tag which, Fn &&fn, Args &&...args); // (16) +``` + +:include-doxygen-doc: fn::expected::expected { args: "" } + +:include-doxygen-doc: fn::expected::expected { args: "U &&" } + +:include-doxygen-doc: fn::expected::expected { args: "::fn::unexpected< G > const &" } + +:include-doxygen-doc: fn::expected::expected { args: "::fn::unexpected< G > &&" } + +:include-doxygen-doc: fn::expected::expected { args: "::std::in_place_t, Args &&..." } + +:include-doxygen-doc: fn::expected::expected { args: "::std::in_place_t, ::std::initializer_list< U >, Args &&..." } + +:include-doxygen-doc: fn::expected::expected { args: "::fn::unexpect_t, Args &&..." } + +:include-doxygen-doc: fn::expected::expected { args: "::fn::unexpect_t, ::std::initializer_list< U >, Args &&..." } + +:include-doxygen-doc: fn::expected::expected { args: "expected const &" } + +:include-doxygen-doc: fn::expected::expected { args: "expected &&" } + +```cpp {title: "fn::expected< void, Err >::expected"} +constexpr expected(); // (1) + +template +constexpr explicit expected(expected const &s); // (2) +constexpr explicit expected(expected &&s); // (3) + +template +constexpr explicit expected(::fn::unexpected const &g); // (4) +constexpr explicit expected(::fn::unexpected &&g); // (5) + +constexpr explicit expected(std::in_place_t); // (6) + +template +constexpr explicit expected(::fn::unexpect_t, Args &&...a); // (7) + +template +constexpr explicit expected(::fn::unexpect_t, std::initializer_list il, Args &&...a); // (8) + +constexpr expected(expected const &) = delete; // (9) +constexpr expected(expected const &) = default; // (10) +constexpr expected(expected const &s); // (11) +constexpr expected(expected &&s) noexcept = default; // (12) +constexpr expected(expected &&s); // (13) + +template +constexpr explicit expected(::pfn::detail::_expected_from_invoke_t tag, Tag which, Fn &&fn, Args &&...args); // (14) +``` + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "" } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "::fn::unexpected< G > const &" } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "::fn::unexpected< G > &&" } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "::std::in_place_t" } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "::fn::unexpect_t, Args &&..." } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "::fn::unexpect_t, ::std::initializer_list< U >, Args &&..." } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "expected const &" } + +:include-doxygen-doc: fn::expected< void, Err >::expected { args: "expected &&" } + +## Destructor {style: "api"} + +```cpp {title: "fn::expected::~expected"} +constexpr ~expected() = default; // (1) +``` + +:include-doxygen-doc: fn::expected::~expected { args: "" } + +```cpp {title: "fn::expected< void, Err >::~expected"} +constexpr ~expected() = default; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::~expected { args: "" } + +## Assignment {style: "api"} + +```cpp {title: "fn::expected::operator="} +template +constexpr auto operator=(U &&s) -> expected &; // (1) + +template +constexpr auto operator=(::fn::unexpected const &s) -> expected &; // (2) +constexpr auto operator=(::fn::unexpected &&s) -> expected &; // (3) + +constexpr auto operator=(expected const &) = delete -> expected &; // (4) +constexpr auto operator=(expected const &) = default -> expected &; // (5) +constexpr auto operator=(expected const &s) -> expected &; // (6) +constexpr auto operator=(expected &&) = default -> expected &; // (7) +constexpr auto operator=(expected &&s) -> expected &; // (8) +``` + +:include-doxygen-doc: fn::expected::operator= { args: "U &&" } + +:include-doxygen-doc: fn::expected::operator= { args: "::fn::unexpected< G > const &" } + +:include-doxygen-doc: fn::expected::operator= { args: "::fn::unexpected< G > &&" } + +:include-doxygen-doc: fn::expected::operator= { args: "expected const &" } + +:include-doxygen-doc: fn::expected::operator= { args: "expected &&" } + +```cpp {title: "fn::expected< void, Err >::operator="} +template +constexpr auto operator=(::fn::unexpected const &s) -> expected &; // (1) +constexpr auto operator=(::fn::unexpected &&s) -> expected &; // (2) + +constexpr auto operator=(expected const &) = delete -> expected &; // (3) +constexpr auto operator=(expected const &) = default -> expected &; // (4) +constexpr auto operator=(expected const &s) -> expected &; // (5) +constexpr auto operator=(expected &&) = default -> expected &; // (6) +constexpr auto operator=(expected &&s) -> expected &; // (7) +``` + +:include-doxygen-doc: fn::expected< void, Err >::operator= { args: "::fn::unexpected< G > const &" } + +:include-doxygen-doc: fn::expected< void, Err >::operator= { args: "::fn::unexpected< G > &&" } + +:include-doxygen-doc: fn::expected< void, Err >::operator= { args: "expected const &" } + +:include-doxygen-doc: fn::expected< void, Err >::operator= { args: "expected &&" } + +## swap {style: "api"} + +```cpp {title: "fn::expected::swap"} +constexpr auto swap(expected &rhs) -> void; // (1) +``` + +:include-doxygen-doc: fn::expected::swap { args: "expected &" } + +```cpp {title: "fn::expected< void, Err >::swap"} +constexpr auto swap(expected &rhs) -> void; // (1) +``` + +:include-doxygen-doc: fn::expected< void, Err >::swap { args: "expected &" } + +## expected_unit {style: "api"} + +The graded gateway: initiating a pipeline with this unit trigger opts all subsequent `and_then` +steps into graded error-set unioning, with no fake starting errors. + +```cpp {title: "fn::expected_unit"} +using expected_unit = expected>; // (1) +``` + +:include-doxygen-doc: fn::expected_unit { args: "" } + +## copack_error {style: "api"} + +The explicit lift into the graded world, on the error side. + +```cpp {title: "fn::expected::copack_error"} +constexpr auto copack_error() const & -> expected>; // (1) +constexpr auto copack_error() && -> expected>; // (2) +constexpr auto copack_error() & -> decltype(auto); // (3) +constexpr auto copack_error() const & -> decltype(auto); // (4) +constexpr auto copack_error() && -> decltype(auto); // (5) +constexpr auto copack_error() const && -> decltype(auto); // (6) +``` + +:include-doxygen-doc: fn::expected::copack_error { args: "" } + +:include-doxygen-doc-params: fn::expected::copack_error { args: "", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::copack_error"} +constexpr auto copack_error() const & -> expected>; // (1) +constexpr auto copack_error() && -> expected>; // (2) +constexpr auto copack_error() & -> decltype(auto); // (3) +constexpr auto copack_error() const & -> decltype(auto); // (4) +constexpr auto copack_error() && -> decltype(auto); // (5) +constexpr auto copack_error() const && -> decltype(auto); // (6) +``` + +:include-doxygen-doc: fn::expected< void, Err >::copack_error { args: "" } + +:include-doxygen-doc-params: fn::expected< void, Err >::copack_error { args: "", title: "parameters" } + +## copack_value {style: "api"} + +The same lift, on the value side. + +```cpp {title: "fn::expected::copack_value"} +constexpr auto copack_value() const & -> expected, error_type>; // (1) +constexpr auto copack_value() && -> expected, error_type>; // (2) +constexpr auto copack_value() & -> decltype(auto); // (3) +constexpr auto copack_value() const & -> decltype(auto); // (4) +constexpr auto copack_value() && -> decltype(auto); // (5) +constexpr auto copack_value() const && -> decltype(auto); // (6) +``` + +:include-doxygen-doc: fn::expected::copack_value { args: "" } + +:include-doxygen-doc-params: fn::expected::copack_value { args: "", title: "parameters" } + +## and_then {style: "api"} + +```cpp {title: "fn::expected::and_then"} +template +constexpr auto and_then(F &&f) &; // (1) +constexpr auto and_then(F &&f) &&; // (2) +constexpr auto and_then(F &&f) const &; // (3) +constexpr auto and_then(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::and_then { args: "F &&" } + +:include-doxygen-doc-params: fn::expected::and_then { args: "F &&", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::and_then"} +template +constexpr auto and_then(F &&f) &; // (1) +constexpr auto and_then(F &&f) &&; // (2) +constexpr auto and_then(F &&f) const &; // (3) +constexpr auto and_then(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::and_then { args: "F &&" } + +:include-doxygen-doc-params: fn::expected< void, Err >::and_then { args: "F &&", title: "parameters" } + +## or_else {style: "api"} + +```cpp {title: "fn::expected::or_else"} +template +constexpr auto or_else(F &&f) &; // (1) +constexpr auto or_else(F &&f) &&; // (2) +constexpr auto or_else(F &&f) const &; // (3) +constexpr auto or_else(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::or_else { args: "F &&" } + +:include-doxygen-doc-params: fn::expected::or_else { args: "F &&", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::or_else"} +template +constexpr auto or_else(F &&f) &; // (1) +constexpr auto or_else(F &&f) &&; // (2) +constexpr auto or_else(F &&f) const &; // (3) +constexpr auto or_else(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::or_else { args: "F &&" } + +:include-doxygen-doc-params: fn::expected< void, Err >::or_else { args: "F &&", title: "parameters" } + +## transform {style: "api"} + +```cpp {title: "fn::expected::transform"} +template +constexpr auto transform(F &&f) &; // (1) +constexpr auto transform(F &&f) &&; // (2) +constexpr auto transform(F &&f) const &; // (3) +constexpr auto transform(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::transform { args: "F &&" } + +:include-doxygen-doc-params: fn::expected::transform { args: "F &&", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::transform"} +template +constexpr auto transform(F &&f) &; // (1) +constexpr auto transform(F &&f) &&; // (2) +constexpr auto transform(F &&f) const &; // (3) +constexpr auto transform(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::transform { args: "F &&" } + +:include-doxygen-doc-params: fn::expected< void, Err >::transform { args: "F &&", title: "parameters" } + +## transform_error {style: "api"} + +```cpp {title: "fn::expected::transform_error"} +template +constexpr auto transform_error(F &&f) &; // (1) +constexpr auto transform_error(F &&f) &&; // (2) +constexpr auto transform_error(F &&f) const &; // (3) +constexpr auto transform_error(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::transform_error { args: "F &&" } + +:include-doxygen-doc-params: fn::expected::transform_error { args: "F &&", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::transform_error"} +template +constexpr auto transform_error(F &&f) &; // (1) +constexpr auto transform_error(F &&f) &&; // (2) +constexpr auto transform_error(F &&f) const &; // (3) +constexpr auto transform_error(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::transform_error { args: "F &&" } + +:include-doxygen-doc-params: fn::expected< void, Err >::transform_error { args: "F &&", title: "parameters" } + +## apply {style: "api"} + +```cpp {title: "fn::expected::apply"} +template +constexpr auto apply(F &&f, Args &&...args) &; // (1) +constexpr auto apply(F &&f, Args &&...args) &&; // (2) +constexpr auto apply(F &&f, Args &&...args) const &; // (3) +constexpr auto apply(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::apply { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected::apply { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::apply"} +template +constexpr auto apply(F &&f, Args &&...args) &; // (1) +constexpr auto apply(F &&f, Args &&...args) &&; // (2) +constexpr auto apply(F &&f, Args &&...args) const &; // (3) +constexpr auto apply(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::apply { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply { args: "F &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::expected::apply_r"} +template +constexpr auto apply_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::apply_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected::apply_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::expected::apply_r { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::apply_r"} +template +constexpr auto apply_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::apply_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply_r { args: "F &&, Args &&...", title: "parameters" } + +## apply_type {style: "api"} + +```cpp {title: "fn::expected::apply_type"} +template +constexpr auto apply_type(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::apply_type { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected::apply_type { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::apply_type"} +template +constexpr auto apply_type(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::apply_type { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply_type { args: "F &&, Args &&...", title: "parameters" } + +## apply_type_r {style: "api"} + +```cpp {title: "fn::expected::apply_type_r"} +template +constexpr auto apply_type_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected::apply_type_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected::apply_type_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::expected::apply_type_r { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::expected< void, Err >::apply_type_r"} +template +constexpr auto apply_type_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::expected< void, Err >::apply_type_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply_type_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::expected< void, Err >::apply_type_r { args: "F &&, Args &&...", title: "parameters" } + +## Free lifts {style: "api"} + +```cpp {title: "fn::copack_error"} +constexpr auto copack_error(some_expected auto &&src) -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::copack_error { args: "some_expected auto &&" } + +:include-doxygen-doc-params: fn::copack_error { args: "some_expected auto &&", title: "parameters" } + +```cpp {title: "fn::copack_value"} +constexpr auto copack_value(some_expected_non_void auto &&src) -> decltype(auto); // (1) +constexpr auto copack_value(some_optional auto &&src) -> decltype(auto); // (2) +``` + +:include-doxygen-doc: fn::copack_value { args: "some_expected_non_void auto &&" } + +:include-doxygen-doc-params: fn::copack_value { args: "some_expected_non_void auto &&", title: "parameters" } diff --git a/docs/reference/fail.md b/docs/reference/fail.md new file mode 100644 index 00000000..8a78f17d --- /dev/null +++ b/docs/reference/fail.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::fail" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::fail_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::fail"} +fail_t fail = {}; // (1) +``` + +:include-doxygen-doc: fn::fail { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::fail_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::fail_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::fail_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/filter.md b/docs/reference/filter.md new file mode 100644 index 00000000..201bbcc3 --- /dev/null +++ b/docs/reference/filter.md @@ -0,0 +1,68 @@ +--- +title: "functor fn::filter" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::filter_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::filter"} +filter_t filter = {}; // (1) +``` + +:include-doxygen-doc: fn::filter { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::filter_t::operator()"} +constexpr auto operator()(auto &&pred, auto &&on_err) const -> functor; // (1) +constexpr auto operator()(auto &&pred) const -> functor; // (2) +``` + +:include-doxygen-doc: fn::filter_t::operator() { args: "auto &&, auto &&" } + +:include-doxygen-doc-params: fn::filter_t::operator() { args: "auto &&, auto &&", title: "parameters" } + +:include-doxygen-doc: fn::filter_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::filter_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. + +--- + +## Examples {style: "api"} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-expected-filter-value"], + desc: "The resulting value is `42` because the filter predicate returns `true` for `42` as it is not less than `42`." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-error-struct", "// example-expected-filter-error"], + desc: "The error is set to `Less than 42` because the predicate returns `false` for `12` since it's less than `42`." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-optional-filter-value"], + desc: "The resulting value is `42` because the filter predicate returns `true` for `42` as it is not less than `42`." +} + +:include-template: templates/snippet.md { + path: "simple/main.cpp", + surroundedBy: ["// example-optional-filter-empty"], + desc: "The optional is empty because the predicate returns `false` for `12` since it's less than `42`." +} diff --git a/docs/reference/functor.md b/docs/reference/functor.md new file mode 100644 index 00000000..99bef316 --- /dev/null +++ b/docs/reference/functor.md @@ -0,0 +1,28 @@ +--- +title: "other fn::functor" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +What a verb call such as `fn::and_then(f)` returns, and the point at which the pipeline is open to +extension: a verb of your own, defined outside the library, pipes exactly like the built-in ones. +A verb is an empty, default-constructible type whose `operator()` returns a `functor` over itself, +and whose nested `apply` does the work; `fn::discard_t` is the smallest example to read. + +--- + +:include-doxygen-doc: fn::functor + +--- + +## Feeding a carrier into a step {style: "api"} + +```cpp {title: "fn::functor::operator|"} +friend constexpr auto operator|(some_monadic_type auto &&v, auto &&self); // (1) +``` + +:include-doxygen-doc: fn::functor::operator| { args: "some_monadic_type auto &&, auto &&" } + +:include-doxygen-doc-params: fn::functor::operator| { args: "some_monadic_type auto &&, auto &&", title: "parameters" } diff --git a/docs/reference/inspect.md b/docs/reference/inspect.md new file mode 100644 index 00000000..b647df02 --- /dev/null +++ b/docs/reference/inspect.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::inspect" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::inspect_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::inspect"} +inspect_t inspect = {}; // (1) +``` + +:include-doxygen-doc: fn::inspect { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::inspect_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::inspect_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::inspect_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/inspect_error.md b/docs/reference/inspect_error.md new file mode 100644 index 00000000..d6d7213c --- /dev/null +++ b/docs/reference/inspect_error.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::inspect_error" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::inspect_error_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::inspect_error"} +inspect_error_t inspect_error = {}; // (1) +``` + +:include-doxygen-doc: fn::inspect_error { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::inspect_error_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::inspect_error_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::inspect_error_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/just.md b/docs/reference/just.md new file mode 100644 index 00000000..02245e4d --- /dev/null +++ b/docs/reference/just.md @@ -0,0 +1,286 @@ +--- +title: "monad fn::just" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::just + +## Member types {style: "api"} + +```cpp {title: "fn::just::value_type"} +using value_type = T; // (1) +``` + +:include-doxygen-doc: fn::just::value_type { args: "" } + +```cpp {title: "fn::just::v_"} +T v_; // (1) +``` + +:include-doxygen-doc: fn::just::v_ { args: "" } + +```cpp {title: "fn::just< void >::value_type"} +using value_type = void; // (1) +``` + +:include-doxygen-doc: fn::just< void >::value_type { args: "" } + +## Construction {style: "api"} + +```cpp {title: "fn::just< void >::just"} +constexpr just() = default; // (1) +constexpr explicit just(std::in_place_type_t); // (2) +constexpr explicit just(std::in_place_t); // (3) +``` + +:include-doxygen-doc: fn::just< void >::just { args: "" } + +:include-doxygen-doc: fn::just< void >::just { args: "::std::in_place_type_t< void >" } + +:include-doxygen-doc: fn::just< void >::just { args: "::std::in_place_t" } + +```cpp {title: "fn::just::just"} +template +just; // (1) + +constexpr just() = default; // (2) +constexpr just(just const &) = default; // (3) +constexpr just(just &&) = default; // (4) + +template +constexpr just(U &&v); // (5) +constexpr explicit just(U &&v); // (6) + +constexpr explicit just(std::in_place_type_t, auto &&...args); // (7) + +template +constexpr explicit just(detail::_just_from_invoke_t, Fn &&make); // (8) +``` + +:include-doxygen-doc: fn::just::just { args: "" } + +:include-doxygen-doc: fn::just::just { args: "just const &" } + +:include-doxygen-doc: fn::just::just { args: "just &&" } + +:include-doxygen-doc: fn::just::just { args: "U &&" } + +:include-doxygen-doc-params: fn::just::just { args: "U &&", title: "parameters" } + +:include-doxygen-doc: fn::just::just { args: "::std::in_place_type_t< T >, auto &&..." } + +:include-doxygen-doc-params: fn::just::just { args: "::std::in_place_type_t< T >, auto &&...", title: "parameters" } + +## Destructor {style: "api"} + +```cpp {title: "fn::just::~just"} +constexpr ~just() = default; // (1) +``` + +:include-doxygen-doc: fn::just::~just { args: "" } + +## emplace {style: "api"} + +```cpp {title: "fn::just::emplace"} +constexpr auto emplace(auto &&...args) -> T &; // (1) +``` + +:include-doxygen-doc: fn::just::emplace { args: "auto &&..." } + +:include-doxygen-doc-params: fn::just::emplace { args: "auto &&...", title: "parameters" } + +## Assignment {style: "api"} + +```cpp {title: "fn::just::operator="} +constexpr auto operator=(just const &) = default -> just &; // (1) +constexpr auto operator=(just &&) = default -> just &; // (2) + +template +constexpr auto operator=(U &&v) -> just &; // (3) +``` + +:include-doxygen-doc: fn::just::operator= { args: "just const &" } + +:include-doxygen-doc: fn::just::operator= { args: "just &&" } + +:include-doxygen-doc: fn::just::operator= { args: "U &&" } + +:include-doxygen-doc-params: fn::just::operator= { args: "U &&", title: "parameters" } + +## operator== {style: "api"} + +```cpp {title: "fn::just< void >::operator=="} +constexpr auto operator==(just const &) const noexcept = default -> bool; // (1) +``` + +:include-doxygen-doc: fn::just< void >::operator== { args: "just const &" } + +## value {style: "api"} + +The payload, always present: the access is total, never throwing. + +```cpp {title: "fn::just::value"} +constexpr auto value() & -> T &; // (1) +constexpr auto value() const & -> T const &; // (2) +constexpr auto value() && -> T &&; // (3) +constexpr auto value() const && -> T const &&; // (4) +``` + +:include-doxygen-doc: fn::just::value { args: "" } + +:include-doxygen-doc-params: fn::just::value { args: "", title: "parameters" } + +```cpp {title: "fn::just< void >::value"} +constexpr auto value() const -> void; // (1) +``` + +:include-doxygen-doc: fn::just< void >::value { args: "" } + +## transform {style: "api"} + +```cpp {title: "fn::just::transform"} +template +constexpr auto transform(Fn &&fn) &; // (1) +constexpr auto transform(Fn &&fn) const &; // (2) +constexpr auto transform(Fn &&fn) &&; // (3) +constexpr auto transform(Fn &&fn) const &&; // (4) +``` + +:include-doxygen-doc: fn::just::transform { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just::transform { args: "Fn &&", title: "parameters" } + +```cpp {title: "fn::just< void >::transform"} +template +constexpr auto transform(Fn &&fn) const; // (1) +``` + +:include-doxygen-doc: fn::just< void >::transform { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just< void >::transform { args: "Fn &&", title: "parameters" } + +## and_then {style: "api"} + +```cpp {title: "fn::just::and_then"} +template +constexpr auto and_then(Fn &&fn) &; // (1) +constexpr auto and_then(Fn &&fn) const &; // (2) +constexpr auto and_then(Fn &&fn) &&; // (3) +constexpr auto and_then(Fn &&fn) const &&; // (4) +``` + +:include-doxygen-doc: fn::just::and_then { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just::and_then { args: "Fn &&", title: "parameters" } + +```cpp {title: "fn::just< void >::and_then"} +template +constexpr auto and_then(Fn &&fn) const; // (1) +``` + +:include-doxygen-doc: fn::just< void >::and_then { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just< void >::and_then { args: "Fn &&", title: "parameters" } + +## apply {style: "api"} + +```cpp {title: "fn::just::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) & -> decltype(auto); // (1) +constexpr auto apply(Fn &&fn, Args &&...args) const & -> decltype(auto); // (2) +constexpr auto apply(Fn &&fn, Args &&...args) && -> decltype(auto); // (3) +constexpr auto apply(Fn &&fn, Args &&...args) const && -> decltype(auto); // (4) +``` + +:include-doxygen-doc: fn::just::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just::apply { args: "Fn &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::just< void >::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) const -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::just< void >::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< void >::apply { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::just::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) & -> Ret; // (1) +constexpr auto apply_r(Fn &&fn, Args &&...args) const & -> Ret; // (2) +constexpr auto apply_r(Fn &&fn, Args &&...args) && -> Ret; // (3) +constexpr auto apply_r(Fn &&fn, Args &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::just::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::just< void >::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) const -> Ret; // (1) +``` + +:include-doxygen-doc: fn::just< void >::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< void >::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just< void >::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type {style: "api"} + +```cpp {title: "fn::just::apply_type"} +template +constexpr auto apply_type(Fn &&fn, Args &&...args) & -> decltype(auto); // (1) +constexpr auto apply_type(Fn &&fn, Args &&...args) const & -> decltype(auto); // (2) +constexpr auto apply_type(Fn &&fn, Args &&...args) && -> decltype(auto); // (3) +constexpr auto apply_type(Fn &&fn, Args &&...args) const && -> decltype(auto); // (4) +``` + +:include-doxygen-doc: fn::just::apply_type { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just::apply_type { args: "Fn &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::just< void >::apply_type"} +template +constexpr auto apply_type(Fn &&fn, Args &&...args) const -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::just< void >::apply_type { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< void >::apply_type { args: "Fn &&, Args &&...", title: "parameters" } + +## apply_type_r {style: "api"} + +```cpp {title: "fn::just::apply_type_r"} +template +constexpr auto apply_type_r(Fn &&fn, Args &&...args) & -> Ret; // (1) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const & -> Ret; // (2) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) && -> Ret; // (3) +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::just::apply_type_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::just< void >::apply_type_r"} +template +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const -> Ret; // (1) +``` + +:include-doxygen-doc: fn::just< void >::apply_type_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } diff --git a/docs/reference/optional.md b/docs/reference/optional.md new file mode 100644 index 00000000..4a0cace3 --- /dev/null +++ b/docs/reference/optional.md @@ -0,0 +1,372 @@ +--- +title: "monad fn::optional" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::optional + +## Member types {style: "api"} + +```cpp {title: "fn::optional::value_type"} +using value_type = T; // (1) +``` + +:include-doxygen-doc: fn::optional::value_type { args: "" } + +```cpp {title: "fn::optional::iterator"} +using iterator = detail::_optional_iterator; // (1) +``` + +:include-doxygen-doc: fn::optional::iterator { args: "" } + +```cpp {title: "fn::optional::const_iterator"} +using const_iterator = detail::_optional_iterator; // (1) +``` + +:include-doxygen-doc: fn::optional::const_iterator { args: "" } + +```cpp {title: "fn::optional< T & >::value_type"} +using value_type = T; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::value_type { args: "" } + +```cpp {title: "fn::optional< T & >::iterator"} +using iterator = detail::_optional_iterator; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::iterator { args: "" } + +## Construction {style: "api"} + +```cpp {title: "fn::optional::optional"} +constexpr optional(); // (1) +constexpr optional(std::nullopt_t); // (2) + +template +constexpr explicit optional(optional const &s); // (3) +constexpr explicit optional(optional &&s); // (4) +constexpr explicit optional(U &&v); // (5) + +template +constexpr explicit optional(std::in_place_t, Args &&...a); // (6) + +template +constexpr explicit optional(std::in_place_t, std::initializer_list il, Args &&...a); // (7) + +constexpr optional(optional const &) = delete; // (8) +constexpr optional(optional const &s) = default; // (9) +constexpr optional(optional const &s); // (10) +constexpr optional(optional &&) noexcept = default; // (11) +constexpr optional(optional &&s); // (12) + +template +constexpr explicit optional(::pfn::detail::_optional_from_invoke_t tag, Fn &&fn, Args &&...args); // (13) +``` + +:include-doxygen-doc: fn::optional::optional { args: "" } + +:include-doxygen-doc: fn::optional::optional { args: "::std::nullopt_t" } + +:include-doxygen-doc: fn::optional::optional { args: "optional < U > const &" } + +:include-doxygen-doc: fn::optional::optional { args: "optional < U > &&" } + +:include-doxygen-doc: fn::optional::optional { args: "U &&" } + +:include-doxygen-doc: fn::optional::optional { args: "::std::in_place_t, Args &&..." } + +:include-doxygen-doc: fn::optional::optional { args: "::std::in_place_t, ::std::initializer_list< U >, Args &&..." } + +:include-doxygen-doc: fn::optional::optional { args: "optional const &" } + +:include-doxygen-doc: fn::optional::optional { args: "optional &&" } + +```cpp {title: "fn::optional< T & >::optional"} +constexpr optional() noexcept = default; // (1) +constexpr optional(std::nullopt_t); // (2) +constexpr optional(optional const &rhs) noexcept = default; // (3) + +template +constexpr explicit optional(std::in_place_t, Arg &&arg); // (4) + +template +constexpr explicit optional(U &&u); // (5) +constexpr explicit optional(optional &rhs); // (6) +constexpr explicit optional(optional const &rhs); // (7) +constexpr explicit optional(optional &&rhs); // (8) +constexpr explicit optional(optional const &&rhs); // (9) + +template +constexpr explicit optional(::pfn::detail::_optional_from_invoke_t tag, Fn &&fn, Args &&...args); // (10) +``` + +:include-doxygen-doc: fn::optional< T & >::optional { args: "" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "::std::nullopt_t" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "optional const &" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "::std::in_place_t, Arg &&" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "U &&" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "optional < U > &" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "optional < U > const &" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "optional < U > &&" } + +:include-doxygen-doc: fn::optional< T & >::optional { args: "optional < U > const &&" } + +## Destructor {style: "api"} + +```cpp {title: "fn::optional::~optional"} +constexpr ~optional() = default; // (1) +``` + +:include-doxygen-doc: fn::optional::~optional { args: "" } + +```cpp {title: "fn::optional< T & >::~optional"} +constexpr ~optional() = default; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::~optional { args: "" } + +## Assignment {style: "api"} + +```cpp {title: "fn::optional::operator="} +constexpr auto operator=(std::nullopt_t) -> optional &; // (1) +constexpr auto operator=(optional const &) = delete -> optional &; // (2) +constexpr auto operator=(optional const &) = default -> optional &; // (3) +constexpr auto operator=(optional const &s) -> optional &; // (4) +constexpr auto operator=(optional &&) = default -> optional &; // (5) +constexpr auto operator=(optional &&s) -> optional &; // (6) + +template +constexpr auto operator=(U &&v) -> optional &; // (7) +constexpr auto operator=(optional const &s) -> optional &; // (8) +constexpr auto operator=(optional &&s) -> optional &; // (9) +``` + +:include-doxygen-doc: fn::optional::operator= { args: "::std::nullopt_t" } + +:include-doxygen-doc: fn::optional::operator= { args: "optional const &" } + +:include-doxygen-doc: fn::optional::operator= { args: "optional &&" } + +:include-doxygen-doc: fn::optional::operator= { args: "U &&" } + +:include-doxygen-doc: fn::optional::operator= { args: "optional < U > const &" } + +:include-doxygen-doc: fn::optional::operator= { args: "optional < U > &&" } + +```cpp {title: "fn::optional< T & >::operator="} +constexpr auto operator=(std::nullopt_t) -> optional &; // (1) +constexpr auto operator=(optional const &rhs) noexcept = default -> optional &; // (2) +``` + +:include-doxygen-doc: fn::optional< T & >::operator= { args: "::std::nullopt_t" } + +:include-doxygen-doc: fn::optional< T & >::operator= { args: "optional const &" } + +## swap {style: "api"} + +```cpp {title: "fn::optional::swap"} +constexpr auto swap(optional &rhs) -> void; // (1) +``` + +:include-doxygen-doc: fn::optional::swap { args: "optional &" } + +```cpp {title: "fn::optional< T & >::swap"} +constexpr auto swap(optional &rhs) -> void; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::swap { args: "optional &" } + +## copack_value {style: "api"} + +The explicit lift into the graded world. + +```cpp {title: "fn::optional::copack_value"} +constexpr auto copack_value() const & -> optional>; // (1) +constexpr auto copack_value() && -> optional>; // (2) +constexpr auto copack_value() & -> decltype(auto); // (3) +constexpr auto copack_value() const & -> decltype(auto); // (4) +constexpr auto copack_value() && -> decltype(auto); // (5) +constexpr auto copack_value() const && -> decltype(auto); // (6) +``` + +:include-doxygen-doc: fn::optional::copack_value { args: "" } + +:include-doxygen-doc-params: fn::optional::copack_value { args: "", title: "parameters" } + +## and_then {style: "api"} + +```cpp {title: "fn::optional::and_then"} +template +constexpr auto and_then(F &&f) &; // (1) +constexpr auto and_then(F &&f) &&; // (2) +constexpr auto and_then(F &&f) const &; // (3) +constexpr auto and_then(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::and_then { args: "F &&" } + +:include-doxygen-doc-params: fn::optional::and_then { args: "F &&", title: "parameters" } + +```cpp {title: "fn::optional< T & >::and_then"} +template +constexpr auto and_then(F &&f) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::and_then { args: "F &&" } + +:include-doxygen-doc-params: fn::optional< T & >::and_then { args: "F &&", title: "parameters" } + +## or_else {style: "api"} + +```cpp {title: "fn::optional::or_else"} +template +constexpr auto or_else(F &&f) const &; // (1) +constexpr auto or_else(F &&f) &&; // (2) +``` + +:include-doxygen-doc: fn::optional::or_else { args: "F &&" } + +:include-doxygen-doc-params: fn::optional::or_else { args: "F &&", title: "parameters" } + +```cpp {title: "fn::optional< T & >::or_else"} +template +constexpr auto or_else(F &&f) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::or_else { args: "F &&" } + +:include-doxygen-doc-params: fn::optional< T & >::or_else { args: "F &&", title: "parameters" } + +## transform {style: "api"} + +```cpp {title: "fn::optional::transform"} +template +constexpr auto transform(F &&f) &; // (1) +constexpr auto transform(F &&f) &&; // (2) +constexpr auto transform(F &&f) const &; // (3) +constexpr auto transform(F &&f) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::transform { args: "F &&" } + +:include-doxygen-doc-params: fn::optional::transform { args: "F &&", title: "parameters" } + +```cpp {title: "fn::optional< T & >::transform"} +template +constexpr auto transform(F &&f) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::transform { args: "F &&" } + +:include-doxygen-doc-params: fn::optional< T & >::transform { args: "F &&", title: "parameters" } + +## apply {style: "api"} + +```cpp {title: "fn::optional::apply"} +template +constexpr auto apply(F &&f, Args &&...args) &; // (1) +constexpr auto apply(F &&f, Args &&...args) &&; // (2) +constexpr auto apply(F &&f, Args &&...args) const &; // (3) +constexpr auto apply(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::apply { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional::apply { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::optional< T & >::apply"} +template +constexpr auto apply(F &&f, Args &&...args) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::apply { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional< T & >::apply { args: "F &&, Args &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::optional::apply_r"} +template +constexpr auto apply_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::apply_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional::apply_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::optional::apply_r { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::optional< T & >::apply_r"} +template +constexpr auto apply_r(F &&f, Args &&...args) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::apply_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional< T & >::apply_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::optional< T & >::apply_r { args: "F &&, Args &&...", title: "parameters" } + +## apply_type {style: "api"} + +```cpp {title: "fn::optional::apply_type"} +template +constexpr auto apply_type(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::apply_type { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional::apply_type { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::optional< T & >::apply_type"} +template +constexpr auto apply_type(F &&f, Args &&...args) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::apply_type { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional< T & >::apply_type { args: "F &&, Args &&...", title: "parameters" } + +## apply_type_r {style: "api"} + +```cpp {title: "fn::optional::apply_type_r"} +template +constexpr auto apply_type_r(F &&f, Args &&...args) &; // (1) +constexpr auto apply_type_r(F &&f, Args &&...args) &&; // (2) +constexpr auto apply_type_r(F &&f, Args &&...args) const &; // (3) +constexpr auto apply_type_r(F &&f, Args &&...args) const &&; // (4) +``` + +:include-doxygen-doc: fn::optional::apply_type_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional::apply_type_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::optional::apply_type_r { args: "F &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::optional< T & >::apply_type_r"} +template +constexpr auto apply_type_r(F &&f, Args &&...args) const; // (1) +``` + +:include-doxygen-doc: fn::optional< T & >::apply_type_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: fn::optional< T & >::apply_type_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::optional< T & >::apply_type_r { args: "F &&, Args &&...", title: "parameters" } diff --git a/docs/reference/or_else.md b/docs/reference/or_else.md new file mode 100644 index 00000000..5fd97e6c --- /dev/null +++ b/docs/reference/or_else.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::or_else" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::or_else_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::or_else"} +or_else_t or_else = {}; // (1) +``` + +:include-doxygen-doc: fn::or_else { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::or_else_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::or_else_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::or_else_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/pack.md b/docs/reference/pack.md new file mode 100644 index 00000000..936610e1 --- /dev/null +++ b/docs/reference/pack.md @@ -0,0 +1,141 @@ +--- +title: "type fn::pack" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::pack + +## Member types {style: "api"} + +```cpp {title: "fn::pack::append_type"} +template +using append_type = _impl::template append_type; // (1) +``` + +:include-doxygen-doc: fn::pack::append_type { args: "" } + +## operator== {style: "api"} + +```cpp {title: "fn::pack::operator=="} +constexpr auto operator==(pack const &other) const -> bool; // (1) +``` + +:include-doxygen-doc: fn::pack::operator== { args: "pack const &" } + +## operator<=> {style: "api"} + +```cpp {title: "fn::pack::operator<=>"} +constexpr auto operator<=>(pack const &other) const; // (1) +``` + +:include-doxygen-doc: fn::pack::operator<=> { args: "pack const &" } + +## Append {style: "api"} + +Grows the product without nesting: appending a value adds one field, and appending a pack +splices its fields in. + +```cpp {title: "fn::pack::append"} +template +constexpr auto append(std::in_place_type_t, auto &&...args) & -> append_type; // (1) +constexpr auto append(std::in_place_type_t, auto &&...args) const & -> append_type; // (2) +constexpr auto append(std::in_place_type_t, auto &&...args) && -> append_type; // (3) +constexpr auto append(std::in_place_type_t, auto &&...args) const && -> append_type; // (4) + +template +constexpr auto append(Arg &&arg) & -> append_type; // (5) +constexpr auto append(Arg &&arg) const & -> append_type; // (6) +constexpr auto append(Arg &&arg) && -> append_type; // (7) +constexpr auto append(Arg &&arg) const && -> append_type; // (8) +``` + +:include-doxygen-doc: fn::pack::append { args: "::std::in_place_type_t< T >, auto &&..." } + +:include-doxygen-doc-params: fn::pack::append { args: "::std::in_place_type_t< T >, auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::pack::append { args: "::std::in_place_type_t< T >, auto &&...", title: "parameters" } + +:include-doxygen-doc: fn::pack::append { args: "Arg &&" } + +:include-doxygen-doc-params: fn::pack::append { args: "Arg &&", title: "parameters" } + +## Apply {style: "api"} + +Elimination: the elements spread into a callable as separate arguments. + +```cpp {title: "fn::pack::apply"} +template +constexpr auto apply(Fn &&fn, auto &&...args) & -> decltype(auto); // (1) +constexpr auto apply(Fn &&fn, auto &&...args) const & -> decltype(auto); // (2) +constexpr auto apply(Fn &&fn, auto &&...args) && -> decltype(auto); // (3) +constexpr auto apply(Fn &&fn, auto &&...args) const && -> decltype(auto); // (4) +``` + +:include-doxygen-doc: fn::pack::apply { args: "Fn &&, auto &&..." } + +:include-doxygen-doc-params: fn::pack::apply { args: "Fn &&, auto &&...", title: "parameters" } + +## apply_r {style: "api"} + +```cpp {title: "fn::pack::apply_r"} +template +constexpr auto apply_r(Fn &&fn, auto &&...args) & -> Ret; // (1) +constexpr auto apply_r(Fn &&fn, auto &&...args) const & -> Ret; // (2) +constexpr auto apply_r(Fn &&fn, auto &&...args) && -> Ret; // (3) +constexpr auto apply_r(Fn &&fn, auto &&...args) const && -> Ret; // (4) +``` + +:include-doxygen-doc: fn::pack::apply_r { args: "Fn &&, auto &&..." } + +:include-doxygen-doc-params: fn::pack::apply_r { args: "Fn &&, auto &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::pack::apply_r { args: "Fn &&, auto &&...", title: "parameters" } + +## as_pack {style: "api"} + +```cpp {title: "fn::as_pack"} +constexpr auto as_pack() -> pack<>; // (1) + +template +constexpr auto as_pack(T &&src, Args &&...args) -> pack; // (2) + +template +constexpr auto as_pack(std::type_identity_t src, std::type_identity_t... args) -> pack; // (3) +``` + +:include-doxygen-doc: fn::as_pack { args: "" } + +:include-doxygen-doc-params: fn::as_pack { args: "", title: "parameters" } + +:include-doxygen-doc: fn::as_pack { args: "T &&, Args &&..." } + +:include-doxygen-doc-params: fn::as_pack { args: "T &&, Args &&...", title: "parameters" } + +:include-doxygen-doc: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >..." } + +:include-doxygen-doc-params: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >...", title: "parameters" } + +## get {style: "api"} + +```cpp {title: "fn::get"} +template +constexpr auto get(Cp &&c) -> decltype(auto); // (1) + +template +constexpr auto get(P &&p) -> decltype(auto); // (2) +``` + +:include-doxygen-doc: fn::get { args: "Cp &&" } + +:include-doxygen-doc-params: fn::get { args: "Cp &&", title: "parameters" } + +:include-doxygen-doc: fn::get { args: "P &&" } + +:include-doxygen-doc-params: fn::get { args: "P &&", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::get { args: "P &&", title: "parameters" } diff --git a/docs/reference/pfn.md b/docs/reference/pfn.md new file mode 100644 index 00000000..2b110700 --- /dev/null +++ b/docs/reference/pfn.md @@ -0,0 +1,177 @@ +--- +title: "polyfills pfn" +--- + +The library is layered: namespace `pfn` is a faithful polyfill of standard vocabulary types and +utilities as specified for C++26, available to a C++20 compiler, and namespace `fn` builds the +functional-programming extensions on top of it. Every `fn` type with a `pfn` counterpart is a +strict superset of it: a valid program switching from `pfn` to `fn` changes neither compilation +nor behaviour. + +`pfn` polyfills only what C++20 lacks: all of ``, the C++23 and C++26 additions to +`std::optional` (the monadic operations, iterator support, `optional`), `std::apply` in its +SFINAE-friendly C++26 shape together with its applicability traits, `std::invoke_r` and +`std::unreachable`. Names C++20 already has — `std::nullopt`, `std::in_place`, +`std::bad_optional_access` — are used directly and not mirrored. + +The polyfills track the C++ working draft, deviating deliberately in three ways, each noted on +the entity it concerns: + +* where the standard leaves a member's `noexcept` specification unstated, one is derived from + the underlying types; every such clause is marked `// extension` in the source +* the draft's hardened preconditions are checked with an assertion, customizable by defining + `LIBFN_ASSERT` before inclusion +* `expected`'s comparison against a value is declared at namespace scope rather than as a + hidden friend, keeping its constraint deducible + +Members are not restated here. A `pfn` type is the standard's type, member for member, so each +one below names its standard counterpart and links to where it is specified; a second copy of +that specification would only be a second thing to keep true. What this page documents is what +differs: the deviations above, and the entities `pfn` adds because C++20 has no equivalent. + +--- + +## expected {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +:include-doxygen-doc: pfn::expected + +Its members are specified as [`std::expected`](https://en.cppreference.com/w/cpp/utility/expected). + +### expected over void {style: "api"} +The partial specialization serving computations which succeed with no value. + +:include-doxygen-doc: pfn::expected< void, E > + +Its members are specified as [`std::expected`](https://en.cppreference.com/w/cpp/utility/expected). + +### unexpected {style: "api"} + +:include-doxygen-doc: pfn::unexpected + +Its members are specified as [`std::unexpected`](https://en.cppreference.com/w/cpp/utility/expected). + +### unexpect {style: "api"} + +:include-doxygen-doc: pfn::unexpect_t + +Its members are specified as [`std::unexpect_t`](https://en.cppreference.com/w/cpp/utility/expected). + +### bad_expected_access {style: "api"} + +:include-doxygen-doc: pfn::bad_expected_access + +Its members are specified as [`std::bad_expected_access`](https://en.cppreference.com/w/cpp/utility/expected). + +:include-doxygen-doc: pfn::bad_expected_access< void > + +--- + +## optional {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +:include-doxygen-doc: pfn::optional + +Its members are specified as [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional). + +### optional over a reference {style: "api"} + +:include-doxygen-doc: pfn::optional< T & > + +Its members are specified as [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional). + +--- + +## apply {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +```cpp {title: "pfn::apply"} +template +constexpr auto apply(Fn &&fn, Tuple &&t) -> apply_result_t; // (1) +``` + +:include-doxygen-doc: pfn::apply { args: "Fn &&, Tuple &&" } + +:include-doxygen-doc-params: pfn::apply { args: "Fn &&, Tuple &&", title: "parameters" } + +### Applicability traits {style: "api"} +The C++26 traits `apply` is specified through; each also comes in its `_v` (for the two +predicates) or `_t` (for the result) form. + +:include-doxygen-doc: pfn::is_applicable + +:include-doxygen-doc: pfn::is_nothrow_applicable + +:include-doxygen-doc: pfn::apply_result + +--- + +## invoke_r {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +```cpp {title: "pfn::invoke_r"} +template +constexpr auto invoke_r(F &&f, Args &&...args); // (1) +``` + +:include-doxygen-doc: pfn::invoke_r { args: "F &&, Args &&..." } + +:include-doxygen-doc-params: pfn::invoke_r { args: "F &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: pfn::invoke_r { args: "F &&, Args &&...", title: "parameters" } + +--- + +## unreachable {style: "api"} + +##### Defined in {style: "api", badge: "#include "} + +```cpp {title: "pfn::unreachable"} +auto unreachable() -> void; // (1) +``` + +:include-doxygen-doc: pfn::unreachable { args: "" } + +## More applicability traits {style: "api"} + +```cpp {title: "pfn::apply_result_t"} +template +using apply_result_t = typename apply_result::type; // (1) +``` + +:include-doxygen-doc: pfn::apply_result_t { args: "" } + +```cpp {title: "pfn::is_applicable_v"} +template +constexpr bool is_applicable_v = is_applicable::value; // (1) +``` + +:include-doxygen-doc: pfn::is_applicable_v { args: "" } + +```cpp {title: "pfn::is_nothrow_applicable_v"} +template +constexpr bool is_nothrow_applicable_v = is_nothrow_applicable::value; // (1) +``` + +:include-doxygen-doc: pfn::is_nothrow_applicable_v { args: "" } + +## Comparison against a value {style: "api"} + +```cpp {title: "pfn::operator=="} +template +constexpr auto operator==(expected const &x, T2 const &v) -> bool; // (1) + +template +constexpr auto operator==(optional const &, optional const &) -> bool; // (2) + +template +constexpr auto operator==(optional const &, std::nullopt_t) -> bool; // (3) + +template +constexpr auto operator==(optional const &, U const &) -> bool; // (4) +constexpr auto operator==(T const &, optional const &) -> bool; // (5) +``` diff --git a/docs/reference/recover.md b/docs/reference/recover.md new file mode 100644 index 00000000..d6105cb7 --- /dev/null +++ b/docs/reference/recover.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::recover" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::recover_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::recover"} +recover_t recover = {}; // (1) +``` + +:include-doxygen-doc: fn::recover { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::recover_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::recover_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::recover_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/transform.md b/docs/reference/transform.md new file mode 100644 index 00000000..6c4b1ee9 --- /dev/null +++ b/docs/reference/transform.md @@ -0,0 +1,35 @@ +--- +title: "functor fn::transform" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::transform_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::transform"} +transform_t transform = {}; // (1) +``` + +:include-doxygen-doc: fn::transform { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::transform_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::transform_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::transform_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. diff --git a/docs/reference/transform_error.md b/docs/reference/transform_error.md new file mode 100644 index 00000000..efd8542e --- /dev/null +++ b/docs/reference/transform_error.md @@ -0,0 +1,38 @@ +--- +title: "functor fn::transform_error" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::transform_error_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::transform_error"} +transform_error_t transform_error = {}; // (1) +``` + +:include-doxygen-doc: fn::transform_error { args: "" } + +## Call signatures {style: "api"} + +```cpp {title: "fn::transform_error_t::operator()"} +constexpr auto operator()(auto &&fn) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::transform_error_t::operator() { args: "auto &&" } + +:include-doxygen-doc-params: fn::transform_error_t::operator() { args: "auto &&", title: "parameters" } + +--- + +## Return value {style: "api"} + +A monadic type of the same kind. + +On `optional` the operation is rejected: an `optional` has no error value to map. Use `or_else` +to act on the empty state instead. diff --git a/docs/reference/utility.md b/docs/reference/utility.md new file mode 100644 index 00000000..38d4413a --- /dev/null +++ b/docs/reference/utility.md @@ -0,0 +1,66 @@ +--- +title: "other fn utilities" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +The small helpers the library exposes for use alongside the carriers: how an argument is stored +when a pipeline step holds it, how to forward with a borrowed value category, how to fuse +callables into one overload set, and how to lift a value into a type that prefers braces. + +--- + +## overload {style: "api"} + +Fuses per-alternative lambdas into a single overload set, which `fn::apply` and the verbs then +dispatch over by ordinary overload resolution. + +```cpp {title: "fn::overload"} +template +overload(Ts const &...) -> overload; // (1) +``` + +--- + +## as_value_t {style: "api"} + +```cpp {title: "fn::as_value_t"} +template +using as_value_t = decltype(detail::_as_value); // (1) +``` + +:include-doxygen-doc: fn::as_value_t { args: "" } + +:include-doxygen-doc-params: fn::as_value_t { args: "", type: "template", title: "template parameters" } + +--- + +## apply_const_lvalue {style: "api"} + +```cpp {title: "fn::apply_const_lvalue"} +template +constexpr auto apply_const_lvalue(auto &&v) -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::apply_const_lvalue { args: "auto &&" } + +:include-doxygen-doc-params: fn::apply_const_lvalue { args: "auto &&", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::apply_const_lvalue { args: "auto &&", title: "parameters" } + +--- + +## make {style: "api"} + +```cpp {title: "fn::make"} +template +constexpr auto make(Args &&...args) -> T; // (1) +``` + +:include-doxygen-doc: fn::make { args: "Args &&..." } + +:include-doxygen-doc-params: fn::make { args: "Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::make { args: "Args &&...", title: "parameters" } diff --git a/docs/reference/value_or.md b/docs/reference/value_or.md new file mode 100644 index 00000000..eb67bd17 --- /dev/null +++ b/docs/reference/value_or.md @@ -0,0 +1,34 @@ +--- +title: "functor fn::value_or" +--- + +##### Defined in {style: "api", badge: "#include "} + +--- + +:include-doxygen-doc: fn::value_or_t + +--- + +## The verb object {style: "api"} + +```cpp {title: "fn::value_or"} +value_or_t value_or = {}; // (1) +``` + +:include-doxygen-doc: fn::value_or { args: "" } + +## Return value {style: "api"} + +The value of the monadic type if present; otherwise the user-provided fallback value. + +## Call signatures {style: "api"} + +```cpp {title: "fn::value_or_t::operator()"} +template +constexpr auto operator()(Args &&...args) const -> functor; // (1) +``` + +:include-doxygen-doc: fn::value_or_t::operator() { args: "Args &&..." } + +:include-doxygen-doc-params: fn::value_or_t::operator() { args: "Args &&...", title: "parameters" } diff --git a/docs/toc b/docs/toc index df0f2841..26db745a 100644 --- a/docs/toc +++ b/docs/toc @@ -1,47 +1,28 @@ -expected - index +reference {title: "REFERENCE"} + pack + copack + expected + optional + just + choice and_then - discard - fail - filter - inspect_error - inspect + transform + transform_error or_else recover - transform_error - transform - value_or -optional - index - and_then - discard - fail filter - inspect_error inspect - or_else - recover - transform_error - transform + inspect_error + fail + discard value_or -choice - index - and_then - transform -pack - index -copack - index -just - index -composition - index -multidispatch - index -pfn - index - expected - optional + conjoin + disjoin + apply + functor + concepts utility -ci + comparison + pfn +continuous-integration {title: "CONTINUOUS INTEGRATION"} index diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d3b5b793..6409d43e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -4,3 +4,4 @@ add_subdirectory(calculator) add_subdirectory(polygon) add_subdirectory(readme) add_subdirectory(simple) +add_subdirectory(type_algebra) diff --git a/examples/readme/main.cpp b/examples/readme/main.cpp index b7d6c29e..fbcb727d 100644 --- a/examples/readme/main.cpp +++ b/examples/readme/main.cpp @@ -91,7 +91,7 @@ constexpr auto parse(std::string_view s) noexcept return fn::pack{n, d}; } -// readme-example +// sync-example-readme // Various error types. enum class NotANumber {}; enum class DivByZero {}; @@ -112,9 +112,9 @@ class Rational { constexpr Rational(int n, int d) noexcept : n_(n), d_(d) {} public: - constexpr bool operator==(Rational const &) const noexcept = default; - constexpr int num() const noexcept { return n_; } - constexpr int den() const noexcept { return d_; } + constexpr auto operator==(Rational const &) const noexcept -> bool = default; + constexpr auto num() const noexcept -> int { return n_; } + constexpr auto den() const noexcept -> int { return d_; } // The invariants live in the type: `make` is the only way to build a `Rational`, and every one is // reduced, sign-normalized and representable. Callers receive a value they never need re-check. @@ -137,28 +137,28 @@ class Rational { return Rational(static_cast(n), static_cast(d)); } - constexpr auto operator()(std::string_view s) const noexcept + constexpr auto operator()(std::string_view s) const noexcept -> decltype(auto) { return parse(s) | fn::and_then(*this); } } make{}; - constexpr auto neg() const noexcept { return make(-1LL * n_, d_); } - constexpr auto inv() const noexcept { return make(d_, n_); } - constexpr auto add(Rational const &other) const noexcept + constexpr auto neg() const noexcept -> decltype(auto) { return make(-1LL * n_, d_); } + constexpr auto inv() const noexcept -> decltype(auto) { return make(d_, n_); } + constexpr auto add(Rational const &other) const noexcept -> decltype(auto) { return make(1LL * n_ * other.d_ + 1LL * other.n_ * d_, // 1LL * d_ * other.d_); } - constexpr auto sub(Rational const &other) const noexcept + constexpr auto sub(Rational const &other) const noexcept -> decltype(auto) { return other.neg() | fn::and_then([*this](Rational y) { return add(y); }); } - constexpr auto mul(Rational const &other) const noexcept + constexpr auto mul(Rational const &other) const noexcept -> decltype(auto) { return make(1LL * n_ * other.n_, 1LL * d_ * other.d_); } - constexpr auto div(Rational const &other) const noexcept + constexpr auto div(Rational const &other) const noexcept -> decltype(auto) { return other.inv() | fn::and_then([*this](Rational y) { return mul(y); }); } @@ -167,7 +167,7 @@ class Rational { // `evaluate` parses each operand, applies the operator, and lets `make` re-check the result. // Each stage fails its own way, and the library folds error types into one co-product. constexpr auto evaluate(std::string_view a, fn::copack_for op, - std::string_view b) noexcept + std::string_view b) noexcept -> decltype(auto) { using Op = fn::expected>; return (Rational::make(a) & Op{op} & Rational::make(b)) // @@ -187,7 +187,7 @@ static_assert( // Constant evaluated calculations used to verify both values and errors during compilation: static_assert(evaluate("1/2", Add{}, "1/3").value() == Rational::make(5, 6)); static_assert(evaluate("2/3", Div{}, "0/1").error().has_value()); -// readme-example +// sync-example-readme int main() { diff --git a/examples/type_algebra/.clang-format b/examples/type_algebra/.clang-format new file mode 100644 index 00000000..c144ef14 --- /dev/null +++ b/examples/type_algebra/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: InheritParentConfig +AllowShortIfStatementsOnASingleLine: WithoutElse +ColumnLimit: 100 diff --git a/examples/type_algebra/CMakeLists.txt b/examples/type_algebra/CMakeLists.txt new file mode 100644 index 00000000..8d7ef1de --- /dev/null +++ b/examples/type_algebra/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.25) +project(examples_type_algebra) + +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Pls keep the filenames sorted +set(EXAMPLES_TYPE_ALGEBRA_SOURCES + main.cpp +) + +foreach(mode 20 23 26) + if (NOT VALIDATE_CXX23 AND mode EQUAL 23) + continue() + endif() + + if (NOT VALIDATE_CXX26 AND mode EQUAL 26) + continue() + endif() + + # Current releases of MSVC only support C++20 + if(MSVC AND NOT (mode EQUAL 20)) + continue() + endif() + + if(mode EQUAL 26) + set(entry_point include_fn_cxx26) + else() + set(entry_point include_fn) + endif() + + set(target "examples_type_algebra_cxx${mode}") + + add_executable("${target}" ${EXAMPLES_TYPE_ALGEBRA_SOURCES}) + target_link_libraries("${target}" "${entry_point}") + append_compilation_options("${target}" WARNINGS) + add_dependencies("cxx${mode}" "${target}") + add_dependencies("examples" "${target}") + add_dependencies("tests" "${target}") + set_property(TARGET "${target}" PROPERTY CXX_STANDARD "${mode}") + target_compile_definitions("${target}" PRIVATE LIBFN_MODE=${mode}) + + add_test( + NAME "${target}" + COMMAND "${target}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + set_property(TEST "${target}" PROPERTY LABELS examples "cxx${mode}") + + unset(target) +endforeach() diff --git a/examples/type_algebra/main.cpp b/examples/type_algebra/main.cpp new file mode 100644 index 00000000..c0aca402 --- /dev/null +++ b/examples/type_algebra/main.cpp @@ -0,0 +1,463 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Error {}; +struct OtherError {}; + +struct A {}; +struct B {}; +struct C {}; +struct D {}; + +// sync-example-types-def +struct UserId {}; +struct User {}; +struct FilePath {}; +struct MaximumSize {}; +struct BlockSize {}; + +struct NotANumber {}; +struct OutOfRange {}; +struct Missing { + auto operator<=>(Missing const &) const = default; +}; +struct IoError {}; +struct BadSyntax {}; +struct UnknownKey {}; +// sync-example-types-def + +// Dummy definitions for non-linked prototypes to prevent linker errors: +fn::expected> parse_id(std::string_view) +{ + return fn::expected>{::fn::unexpect, NotANumber{}}; +} +fn::expected> validate(UserId) +{ + return fn::expected>{::fn::unexpect, OutOfRange{}}; +} +fn::expected> load(UserId) +{ + return fn::expected>{::fn::unexpect, IoError{}}; +} +fn::expected> parse_numeric() +{ + return fn::expected>{::fn::unexpect, NotANumber{}}; +} +fn::expected> load_user(UserId) +{ + return fn::expected>{::fn::unexpect, Missing{}}; +} +fn::expected, + fn::copack_for> +read_config() +{ + return fn::expected, + fn::copack_for>{::fn::unexpect, BadSyntax{}}; +} + +// sync-example-graded-pipeline +auto parse_id(std::string_view) -> fn::expected>; +auto validate(UserId) -> fn::expected>; +auto load(UserId) -> fn::expected>; + +auto graded_pipeline(std::string_view sv) -> void +{ + auto pipeline = parse_id(sv) | fn::and_then(validate) | fn::and_then(load); + + // The exact derived error union is recorded in the type: + static_assert( + std::same_as>>); +} +// sync-example-graded-pipeline + +// sync-example-copack-set-semantics +auto test_copack_set_semantics() -> void +{ + using SetA = fn::copack_for; + using SetB = fn::copack_for; + + // Flattening, deduplication, and reordering happen automatically: + using Union = fn::copack_for>; + + static_assert(std::same_as>); +} +// sync-example-copack-set-semantics + +// sync-example-test-pack +auto test_pack(int x = 12, double d = 3.14) -> void +{ + fn::pack p{UserId{}, User{}}; // CTAD + [[maybe_unused]] auto [id, user] = p; // Structured bindings work naturally + + // Ordered, non-deduplicated fields + using P = fn::pack; + static_assert(std::tuple_size_v

== 3); + + // Found via ADL (like std::get) + using std::get; + static_assert(std::same_as(p)), UserId &>); + + // Splicing scalars or other packs via append: + auto row = fn::pack{UserId{}}.append(FilePath{}); + auto wider = std::move(row).append(fn::pack{true, 3}); + + static_assert(std::same_as>); + + // Explicitly lifting a single scalar value into a pack: + auto lifted_lvalue = fn::as_pack(x); + static_assert(std::same_as>); + + auto lifted_rvalue = fn::as_pack(42); + static_assert(std::same_as>); + + // Spelling the element type explicitly can be used to opt out of reference preservation: + auto copied = fn::as_pack(x); + static_assert(std::same_as>); + // ... or to force a specific reference type (subject to parameter binding rules): + auto referenced = fn::as_pack(x); + static_assert(std::same_as>); + + // The explicit form also coerces - the argument converts at the call boundary: + auto coerced = fn::as_pack(x, d); + static_assert(std::same_as>); +} +// sync-example-test-pack + +// sync-example-test-copack +struct IntegerToken {}; +struct StringToken {}; + +auto test_copack() -> void +{ + static constexpr fn::copack_for token = IntegerToken{}; + + // Member apply eliminates the copack by routing the active alternative to an overload set: + static constexpr auto value + = token.apply(fn::overload{[](IntegerToken) { return 1; }, [](StringToken) { return 2; }}); + static_assert(value == 1); + + // Storing a pack inside a copack is allowed, including a pack holding a reference: + auto cpr = fn::as_copack(fn::as_pack(value)); + static_assert(std::same_as>>); + + // Singular lift and direct value extraction (only allowed for singular copacks): + auto cp = fn::as_copack(42); + using std::get; + static_assert(std::same_as); +} +// sync-example-test-copack + +// sync-example-mapping-values-and-errors +auto mapping_values_and_errors() -> void +{ + fn::expected> ex{}; + + auto mapped_val = ex | fn::transform([](UserId) { return User{}; }); + static_assert( + std::same_as>>); + + auto mapped_err = ex + | fn::transform_error(fn::overload{[](Missing) { return BadSyntax{}; }, + [](IoError e) { return e; }}); + + static_assert( + std::same_as>>); +} +// sync-example-mapping-values-and-errors + +// sync-example-operator-and-composition +auto operator_and_composition(fn::expected a, fn::expected b) -> void +{ + auto result = a & b; + static_assert(std::same_as, fn::copack_for>>); +} +// sync-example-operator-and-composition + +// sync-example-cartesian-distribution +auto test_cartesian_distribution(fn::copack_for ab, fn::copack_for cd) -> void +{ + // Cartesian distribution of copacks: (A + B) x (C + D) = (A x C) + (A x D) + (B x C) + (B x D) + auto result1 = ab & cd; + static_assert( + std::same_as, fn::pack, fn::pack, fn::pack>>); + + // Cartesian distribution of a pack and a copack: (A x B) x (C + D) = (A x B x C) + (A x B x D) + constexpr fn::pack Pab = {A{}, B{}}; + auto result2 = Pab & cd; + static_assert( + std::same_as, fn::pack>>); +} +// sync-example-cartesian-distribution + +// sync-example-conjunction-with-identity-cluster +auto test_conjunction_with_identity_cluster(fn::expected ex, fn::just j) -> void +{ + // Conjoining an expected with a just + auto res1 = ex & j; + static_assert(std::same_as, Error>>); + + // Conjoining with a unit (just) completely elides the unit + auto res2 = ex & fn::just{}; + static_assert(std::same_as); + + // Conjoining a choice causes distribution inside the carrier + fn::choice ch = 1.5; + auto res3 = ex & ch; + static_assert(std::same_as< + decltype(res3), + fn::expected, fn::pack>, Error>>); +} +// sync-example-conjunction-with-identity-cluster + +// sync-example-operator-or-composition +auto operator_or_composition(fn::expected a, fn::expected b) -> void +{ + auto result = a | b; + static_assert(std::same_as, fn::pack>>); +} +// sync-example-operator-or-composition + +// sync-example-test-disjoin +auto test_disjoin(fn::expected a, fn::expected b) -> void +{ + // Multiple fallible operands compose cleanly + auto res1 = fn::disjoin(a, b); + static_assert(std::same_as, fn::pack>>); + + // Because just cannot fail, the entire disjunction with infallible operands becomes total + auto res2 = fn::disjoin(a, b, fn::just{1.5}); + static_assert(std::same_as>); +} +// sync-example-test-disjoin + +// sync-example-sequential-bind +auto parse_numeric() -> fn::expected>; +auto load_user(UserId) -> fn::expected>; + +auto sequential_bind() -> void +{ + auto result = parse_numeric() | fn::and_then(load_user); + static_assert( + std::same_as>>); +} +// sync-example-sequential-bind + +// sync-example-test-same-kind +static_assert(fn::same_kind, fn::optional>); +static_assert(fn::same_kind, fn::expected>); +static_assert(!fn::same_kind, fn::expected>); +// sync-example-test-same-kind + +// sync-example-config-pipeline +auto read_config() -> fn::expected, + fn::copack_for>; + +auto config_pipeline() -> void +{ + auto validated + = read_config() + | fn::and_then(fn::overload{ + [](MaximumSize v) { return fn::expected>{v}; }, + [](FilePath v) { return fn::expected>{v}; }, + [](BlockSize v) { return fn::expected>{v}; }}); + + // The result exactly bounds both the successful paths and the error paths + static_assert( + std::same_as, + fn::copack_for>>); +} +// sync-example-config-pipeline + +// sync-example-test-same-kind-graded +static_assert( + fn::same_kind>, fn::expected>>); +// sync-example-test-same-kind-graded + +// sync-example-test-explicit-lifting +auto test_explicit_lifting(fn::expected result, fn::optional opt) -> void +{ + // Explicitly lift the error side of expected: + auto graded_err = std::move(result).copack_error(); + static_assert(std::same_as>>); + + // Explicitly lift the value side of expected: + auto graded_val = std::move(result).copack_value(); + static_assert(std::same_as, IoError>>); + + // Explicitly lift the value side of optional: + auto graded_opt = std::move(opt).copack_value(); + static_assert(std::same_as>>); +} +// sync-example-test-explicit-lifting + +template using cannot_fail_t = fn::expected>; + +// sync-example-test-identity-cross +auto test_identity_cross() -> void +{ + fn::just j{UserId{}}; + + // Cross-carrier pipeline bind to another identity carrier + auto result = j | fn::and_then([](UserId u) { return fn::expected>{u}; }); + + static_assert(std::same_as>>); +} +// sync-example-test-identity-cross + +// sync-example-test-success-bridge +auto test_success_bridge(fn::just j) -> void +{ + // An identity carrier can bridge to fallible carriers on the success path + auto to_opt = j | fn::and_then([](int i) { return fn::optional{i}; }); + static_assert(std::same_as>); + + auto to_exp = j | fn::and_then([](int i) { return fn::expected{i}; }); + static_assert(std::same_as>); + + // Bridging a multi-alternative choice to fallible optional with heterogeneous success join: + auto choice_to_opt + = fn::choice_for{true} + | fn::and_then(fn::overload{[](int) -> fn::optional { return {'a'}; }, + [](bool) -> fn::optional { return {2L}; }}); + static_assert(std::same_as>>); +} +// sync-example-test-success-bridge + +// sync-example-test-failure-bridge +auto test_failure_bridge(fn::expected ex, fn::optional opt) -> void +{ + // Fallible carriers can bridge to each other on the failure/empty recovery path + auto expected_to_optional = ex | fn::or_else([](IoError) { return fn::optional{}; }); + static_assert(std::same_as>); + + auto optional_to_expected = opt | fn::or_else([]() { return fn::expected{100}; }); + static_assert(std::same_as>); +} +// sync-example-test-failure-bridge + +// sync-example-vacuous-or-else +auto test_vacuous_or_else() -> void +{ + using type = decltype(fn::expected>{} | fn::or_else(std::declval())); + static_assert(std::same_as>>); +} +// sync-example-vacuous-or-else + +// sync-example-test-identity-transformation +auto test_identity_transformation(fn::just j) -> void +{ + // Transforming a just with a callable returning a copack produces a choice + auto mapped + = j | fn::transform([](UserId) { return fn::copack_for{Missing{}}; }); + + static_assert(std::same_as>); +} +// sync-example-test-identity-transformation + +// sync-example-test-choice-mapping +auto test_choice_mapping(fn::choice ch) -> void +{ + constexpr auto mapper = fn::overload{[](UserId) { return fn::choice{Missing{}}; }, + [](User) { return fn::choice{FilePath{}}; }}; + + // transform nests the returned choice as a mapped value + auto mapped = ch | fn::transform(mapper); + static_assert( + std::same_as, fn::choice>>); + + // and_then joins and flattens them into a normalized superset choice + auto bound = ch | fn::and_then(mapper); + static_assert(std::same_as>); +} +// sync-example-test-choice-mapping + +// sync-example-test-elimination +auto test_elimination(fn::expected> ex) -> int +{ + return ex.apply(fn::overload{[](UserId) { return 1; }, [](Missing) { return 0; }}); +} +// sync-example-test-elimination + +// sync-example-test-laws +constexpr auto test_laws() -> void +{ + constexpr fn::expected> ex{42}; + + // Functor Identity: mapping with identity yields the same value + constexpr auto id = [](auto v) { return v; }; + static_assert((ex | fn::transform(id)) == ex); + + // Monad Left Identity: pure(x) >>= f is equivalent to f(x) + constexpr auto pure = [](int v) { return fn::expected>{v}; }; + constexpr auto f = [](int v) { return fn::expected>{v * 2}; }; + static_assert((pure(42) | fn::and_then(f)) == f(42)); +} +// sync-example-test-laws + +// sync-example-test-references +auto test_references() -> void +{ + int x = 42; + + // optional supports references directly + fn::optional opt{x}; + static_assert(std::same_as); + + // expected must wrap references inside a pack + fn::expected, Error> ex{fn::as_pack(x)}; + static_assert(std::same_as &>); +} +// sync-example-test-references + +int main() +{ + // Touch all functions to prove compilability and execution + std::string_view sv = ""; + graded_pipeline(sv); + operator_and_composition({}, {}); + test_copack_set_semantics(); + test_pack(); + test_copack(); + mapping_values_and_errors(); + test_cartesian_distribution(A{}, C{}); + test_conjunction_with_identity_cluster({42}, {1.5}); + operator_or_composition({}, {}); + test_disjoin({12}, {true}); + sequential_bind(); + config_pipeline(); + test_explicit_lifting(fn::expected{User{}}, fn::optional{User{}}); + test_identity_cross(); + test_success_bridge({1}); + test_failure_bridge(fn::expected{}, fn::optional{}); + test_vacuous_or_else(); + test_identity_transformation({UserId{}}); + test_choice_mapping({UserId{}}); + (void)test_elimination(fn::expected>{UserId{}}); + test_laws(); + test_references(); + return 0; +} diff --git a/include/fn/and_then.hpp b/include/fn/and_then.hpp index 1648bf52..4e8bf085 100644 --- a/include/fn/and_then.hpp +++ b/include/fn/and_then.hpp @@ -162,7 +162,7 @@ constexpr inline struct and_then_t final { } struct apply; -} and_then = {}; +} and_then = {}; ///< Binds through a callable returning a carrier: `x | and_then(f)` struct and_then_t::apply final { /** diff --git a/include/fn/choice.hpp b/include/fn/choice.hpp index 62ac1749..e8a1192c 100644 --- a/include/fn/choice.hpp +++ b/include/fn/choice.hpp @@ -60,10 +60,22 @@ struct choice : copack { static_assert((... && detail::_is_valid_choice_subtype)); static_assert(::std::same_as::template apply<::fn::choice>, choice>); using _impl = copack; + /** + * @brief The copack of alternatives this choice carries + */ using value_type = _impl; + /** + * @brief The number of alternatives + */ static constexpr ::std::size_t size = sizeof...(Ts); + /** + * @brief The I-th alternative in the canonical order + */ template <::std::size_t I> using select_nth = detail::select_nth_t; + /** + * @brief Whether `T` is one of the alternatives + */ template static constexpr bool has_type = _impl::template has_type; template @@ -84,6 +96,8 @@ struct choice : copack { /** * @brief Constructs the alternative matching the value's decayed type * + * Explicit exactly where the conversion to that alternative is. + * * @param v Value of one alternative */ template @@ -96,12 +110,6 @@ struct choice : copack { { } - /** - * @brief Constructs the alternative matching the value's decayed type, where that conversion is - * explicit - * - * @param v Value of one alternative - */ template constexpr explicit choice(T &&v) // NOSONAR cpp:S6458 has_type excludes self noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t<::std::remove_cvref_t>, decltype(v)>) @@ -144,6 +152,9 @@ struct choice : copack { { } + /** + * @brief Widening constructor from a copack over a subset of the alternatives + */ template constexpr choice(copack &&v) // NOSONAR cpp:S1709 implicit widening by design noexcept(::std::is_nothrow_constructible_v<_impl, ::std::in_place_type_t>, copack>) @@ -167,21 +178,39 @@ struct choice : copack { { } + /** + * @brief Copy constructor + */ constexpr choice(choice const &other) = default; + /** + * @brief Move constructor + */ constexpr choice(choice &&other) = default; + /** + * @brief Destructor + */ constexpr ~choice() = default; // Declared because the move constructor above would otherwise delete the implicit copy assignment // and suppress the implicit move assignment. Defaulted, so both inherit the base copack's - its // constraints, its strong guarantee, and its computed noexcept (which an explicit one here would // contradict, and thereby delete). + /** + * @brief Copy assignment + */ constexpr choice &operator=(choice const &other) = default; + /** + * @brief Move assignment + */ constexpr choice &operator=(choice &&other) = default; // choice declares its copy and move assignment, and a declared operator= hides every base // overload - copack's widening assignment must be restated here to exist at all. Delegating keeps // the answer copack's, and admits a copack over the same alternatives, which would otherwise pay for // the temporary the widening constructor builds. + /** + * @brief Widening assignment from a copack over a subset of the alternatives + */ template constexpr choice &operator=(copack const &arg) // noexcept(::std::is_nothrow_assignable_v &, copack const &>) @@ -190,6 +219,9 @@ struct choice : copack { static_cast &>(*this) = arg; return *this; } + /** + * @brief Widening assignment from a copack over a subset of the alternatives + */ template constexpr choice &operator=(copack &&arg) // noexcept(::std::is_nothrow_assignable_v &, copack>) @@ -202,6 +234,9 @@ struct choice : copack { // The delegating value assignment restates copack's for the same name-hiding reason. Copack- and // choice-typed sources are excluded to leave them to the assignments above: for a non-const // lvalue source a forwarding reference would otherwise outrank their `const &` bindings. + /** + * @brief Assignment from a value + */ template constexpr choice &operator=(U &&v) // noexcept(::std::is_nothrow_assignable_v &, decltype(v)>) @@ -604,6 +639,26 @@ constexpr inline bool _nothrow_choice_fold // The conjunction inside the cluster, choice on either side: the copack distributes through the // value product, so the fold answers a copack of packs and the result stays a choice. just // is the product's unit and elides. +// The description below covers every `&` over carriers, not just this arm: the reference renders +// one description per parameter-type signature, and all of them share `(Lh &&, Rh &&)`, of which +// this is the first doxygen reports. The arms in expected.hpp and optional.hpp carry theirs as +// ordinary comments for the same reason. +/** + * @brief The conjunction of carriers: values multiply into a `pack`, errors sum into a `copack` + * + * `a & b` succeeds only where both operands do, the values folding into one `pack` - a `void` + * side elides, and a copack value distributes into a copack of packs. What the failure side + * carries depends on the carrier: an `expected` holds the leftmost failing operand's error, an + * identical pair of error types staying as it is and any other pair summing into its normalized + * `copack_for`, grading not required of the operands; an `optional` is simply empty, its unit + * error needing no summing; a `choice` or `just` cannot fail, so the fold is total. Both operands + * are fully constructed before the operator runs: an error-selection rule, not short-circuiting. + * An identity-cluster operand contributes its value and no error term. + * + * @param lh Left operand + * @param rh Right operand + * @return The carrier of the folded value product, over the summed failure side + */ template requires(detail::_some_choice || detail::_some_choice) && (detail::_some_just || detail::_some_choice) && (detail::_some_just || detail::_some_choice) @@ -676,6 +731,23 @@ template // uninhabited factor into the error product, so the result never fails and collapses into the // cluster: just when the value sum stays one bare type, choice when the union is genuine. The // leftmost engaged operand wins; a cluster operand is always engaged. +// As with `&` above, this arm carries the description for every `|` over carriers. +/** + * @brief The disjunction of carriers: values sum into a `copack`, errors multiply into a `pack` + * + * `a | b` yields the leftmost operand that worked, its value injected into the sum of the value + * types - a same-type pair stays bare, and a `void` side enters a genuine sum as `pack<>`. What + * remains when none worked depends on the carrier: an `expected` holds the product of every + * error, all evidence kept positionally; an `optional` is simply empty, its unit errors vanishing + * in that product; a `choice` or `just` always works, so the disjunction is total. Both operands + * are fully constructed before the operator runs: a value-selection rule, not a lazy fallback. An + * identity-cluster operand makes the whole disjunction total, collapsing the result into `just` + * or `choice`. + * + * @param lh Left operand + * @param rh Right operand + * @return The carrier of the summed value side, over the error product + */ template requires(detail::_cluster_operand || detail::_cluster_operand) // && detail::_some_carrier && detail::_some_carrier diff --git a/include/fn/copack.hpp b/include/fn/copack.hpp index 8e675672..d50c381a 100644 --- a/include/fn/copack.hpp +++ b/include/fn/copack.hpp @@ -249,14 +249,38 @@ template struct copack; * type can sit inside a union storage. */ template <> struct copack<> final { + /** + * @brief Default constructor; not available on this carrier + */ constexpr copack() noexcept = delete; // NOTE, `= delete` here is the whole point + /** + * @brief Destructor + */ constexpr ~copack() noexcept = default; + /** + * @brief Copy constructor + */ constexpr copack(copack const &) noexcept = default; + /** + * @brief Move constructor + */ constexpr copack(copack &&) noexcept = default; + /** + * @brief Copy assignment + */ constexpr copack &operator=(copack const &) noexcept = default; + /** + * @brief Move assignment + */ constexpr copack &operator=(copack &&) noexcept = default; + /** + * @brief The number of alternatives + */ static constexpr ::std::size_t size = 0; + /** + * @brief Whether `T` is one of the alternatives + */ template static constexpr bool has_type = false; }; @@ -275,10 +299,22 @@ struct copack { static_assert((... && detail::_is_valid_copack_subtype)); static_assert(::std::same_as::template apply<::fn::copack>, copack>); + /** + * @brief The union holding the alternatives + */ using data_t = detail::variadic_union; + /** + * @brief The union holding the active alternative + */ data_t data; + /** + * @brief The index of the active alternative + */ ::std::size_t index; + /** + * @brief The number of alternatives + */ static constexpr ::std::size_t size = sizeof...(Ts); // What copying and moving a copack cost, asked of the storage that performs them - see the concepts @@ -378,7 +414,8 @@ struct copack { * @brief Constructs the alternative matching the value's decayed type * * Takes a value of exactly one alternative: a merely convertible non-alternative is rejected, - * so interconvertible alternatives never make a resolution puzzle. + * so interconvertible alternatives never make a resolution puzzle. Explicit exactly where the + * conversion to that alternative is. * * @param v Value of one alternative */ @@ -392,12 +429,6 @@ struct copack { { } - /** - * @brief Constructs the alternative matching the value's decayed type, where that conversion is - * explicit - * - * @param v Value of one alternative - */ template constexpr explicit copack(T &&v) // NOSONAR cpp:S6458 has_type excludes self noexcept(detail::_nothrow_makeable, decltype(v)>) @@ -444,6 +475,9 @@ struct copack { { } + /** + * @brief Widening constructor from a copack over a subset of the alternatives + */ template constexpr copack(copack &&arg) // NOSONAR cpp:S1709 implicit widening by design noexcept((... && detail::_nothrow_makeable)) @@ -515,6 +549,9 @@ struct copack { { } + /** + * @brief Destructor + */ constexpr ~copack() requires _trivially_destructible = default; diff --git a/include/fn/discard.hpp b/include/fn/discard.hpp index 4f90aef7..bcb4472a 100644 --- a/include/fn/discard.hpp +++ b/include/fn/discard.hpp @@ -33,7 +33,7 @@ constexpr inline struct discard_t final { struct apply final { constexpr auto operator()(some_monadic_type auto &&) const noexcept -> void {} // NOSONAR cpp:S1186 discards }; -} discard = {}; +} discard = {}; ///< Drops the carrier's content: `x | discard` } // namespace LIBFN_VERSION } // namespace fn diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index e7514513..795b6972 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -904,13 +904,28 @@ template class expected : private detail::_expected_b template friend struct ::fn::detail::_expected_base; public: + /** + * @brief The type of the value side + */ using value_type = T; + /** + * @brief The type of the error side + */ using error_type = Err; + /** + * @brief The `unexpected` specialization over the error type + */ using unexpected_type = ::fn::unexpected; + /** + * @brief This carrier over another value type + */ template using rebind = expected; // Constructors. Explicit forwarders to the base mirror pfn::expected. + /** + * @brief Default constructor + */ constexpr expected() noexcept(::std::is_nothrow_default_constructible_v) requires ::std::is_default_constructible_v : _base(::std::in_place) @@ -919,6 +934,9 @@ template class expected : private detail::_expected_b template constexpr explicit(not ::std::is_convertible_v || not ::std::is_convertible_v) + /** + * @brief Converting constructor from a compatible carrier + */ expected(expected const &s) // noexcept(::std::is_nothrow_constructible_v && ::std::is_nothrow_constructible_v) requires(_base::template _can_copy_convert::value) @@ -927,12 +945,18 @@ template class expected : private detail::_expected_b } template constexpr explicit(not ::std::is_convertible_v || not ::std::is_convertible_v) + /** + * @brief Converting constructor from a compatible carrier + */ expected(expected &&s) // noexcept(::std::is_nothrow_constructible_v && ::std::is_nothrow_constructible_v) requires(_base::template _can_move_convert::value) : _base(::std::move(s)) { } + /** + * @brief Constructs the value from a value + */ template > constexpr explicit(not ::std::is_convertible_v) expected(U &&v) // noexcept(::std::is_nothrow_constructible_v) @@ -941,6 +965,9 @@ template class expected : private detail::_expected_b { } + /** + * @brief Constructs the error from an `unexpected` + */ template constexpr explicit(not ::std::is_convertible_v) expected(::fn::unexpected const &g) // noexcept(::std::is_nothrow_constructible_v) @@ -948,6 +975,9 @@ template class expected : private detail::_expected_b : _base(::fn::unexpect, ::std::forward(g.error())) { } + /** + * @brief Constructs the error from an `unexpected` + */ template constexpr explicit(not ::std::is_convertible_v) expected(::fn::unexpected &&g) // noexcept(::std::is_nothrow_constructible_v) @@ -956,6 +986,9 @@ template class expected : private detail::_expected_b { } + /** + * @brief Constructs the value in place from the arguments + */ template constexpr explicit expected(::std::in_place_t, Args &&...a) // noexcept(::std::is_nothrow_constructible_v) @@ -963,6 +996,9 @@ template class expected : private detail::_expected_b : _base(::std::in_place, FWD(a)...) { } + /** + * @brief Constructs the value in place from the arguments + */ template constexpr explicit expected(::std::in_place_t, ::std::initializer_list il, Args &&...a) // noexcept(::std::is_nothrow_constructible_v &, Args...>) @@ -970,6 +1006,9 @@ template class expected : private detail::_expected_b : _base(::std::in_place, il, FWD(a)...) { } + /** + * @brief Constructs the error in place from the arguments + */ template constexpr explicit expected(::fn::unexpect_t, Args &&...a) // noexcept(::std::is_nothrow_constructible_v) // @@ -977,6 +1016,9 @@ template class expected : private detail::_expected_b : _base(::fn::unexpect, FWD(a)...) { } + /** + * @brief Constructs the error in place from the arguments + */ template constexpr explicit expected(::fn::unexpect_t, ::std::initializer_list il, Args &&...a) // noexcept(::std::is_nothrow_constructible_v &, Args...>) @@ -985,6 +1027,9 @@ template class expected : private detail::_expected_b { } + /** + * @brief Copy constructor; not available on this carrier + */ constexpr expected(expected const &) = delete; constexpr expected(expected const &s) // noexcept(::std::is_nothrow_copy_constructible_v && ::std::is_nothrow_copy_constructible_v) @@ -998,6 +1043,9 @@ template class expected : private detail::_expected_b : _base(s.set_, FWD(s).storage_) { } + /** + * @brief Move constructor + */ constexpr expected(expected &&s) noexcept requires(::std::is_move_constructible_v && ::std::is_move_constructible_v && ::std::is_trivially_move_constructible_v && ::std::is_trivially_move_constructible_v) @@ -1010,9 +1058,15 @@ template class expected : private detail::_expected_b { } + /** + * @brief Destructor + */ constexpr ~expected() = default; // Assignment. Explicit forwarders mirror pfn::expected to avoid an MSVC bug. + /** + * @brief Assignment from a value + */ template constexpr expected &operator=(U &&s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) @@ -1021,6 +1075,9 @@ template class expected : private detail::_expected_b this->_assign_value(FWD(s)); return *this; } + /** + * @brief Assignment from an `unexpected` + */ template constexpr expected &operator=(::fn::unexpected const &s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) @@ -1031,6 +1088,9 @@ template class expected : private detail::_expected_b this->_assign_unexpected(s); return *this; } + /** + * @brief Assignment from an `unexpected` + */ template constexpr expected &operator=(::fn::unexpected &&s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) @@ -1041,6 +1101,9 @@ template class expected : private detail::_expected_b this->_assign_unexpected(::std::move(s)); return *this; } + /** + * @brief Copy assignment; not available on this carrier + */ constexpr expected &operator=(expected const &) = delete; constexpr expected &operator=(expected const &) // noexcept(::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v @@ -1065,6 +1128,9 @@ template class expected : private detail::_expected_b this->_assign(static_cast<_base const &>(s)); return *this; } + /** + * @brief Move assignment + */ constexpr expected &operator=(expected &&) // noexcept(::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v && ::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v) @@ -1105,6 +1171,9 @@ template class expected : private detail::_expected_b // Swap; body delegates to _expected_base helper constexpr void + /** + * @brief Swaps the contents with another `expected` + */ swap(expected &rhs) noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v && ::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v) requires(::std::is_swappable_v && ::std::is_swappable_v && ::std::is_move_constructible_v @@ -1576,14 +1645,32 @@ template class expected : private detail::_expected_ba template friend struct ::fn::detail::_expected_base; public: + /** + * @brief The type of the value side + */ using value_type = void; + /** + * @brief The type of the error side + */ using error_type = Err; + /** + * @brief The `unexpected` specialization over the error type + */ using unexpected_type = ::fn::unexpected; + /** + * @brief This carrier over another value type + */ template using rebind = expected; + /** + * @brief Default constructor + */ constexpr expected() noexcept : _base(::std::in_place) {} + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) expected(expected const &s) // noexcept(::std::is_nothrow_constructible_v) @@ -1591,6 +1678,9 @@ template class expected : private detail::_expected_ba : _base(s) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) expected(expected &&s) // noexcept(::std::is_nothrow_constructible_v) @@ -1598,6 +1688,9 @@ template class expected : private detail::_expected_ba : _base(::std::move(s)) { } + /** + * @brief Constructs the error from an `unexpected` + */ template constexpr explicit(not ::std::is_convertible_v) expected(::fn::unexpected const &g) // noexcept(::std::is_nothrow_constructible_v) @@ -1605,6 +1698,9 @@ template class expected : private detail::_expected_ba : _base(::fn::unexpect, ::std::forward(g.error())) { } + /** + * @brief Constructs the error from an `unexpected` + */ template constexpr explicit(not ::std::is_convertible_v) expected(::fn::unexpected &&g) // noexcept(::std::is_nothrow_constructible_v) @@ -1613,8 +1709,14 @@ template class expected : private detail::_expected_ba { } + /** + * @brief Constructs the value in place from the arguments + */ constexpr explicit expected(::std::in_place_t) noexcept : _base(::std::in_place) {} + /** + * @brief Constructs the error in place from the arguments + */ template constexpr explicit expected(::fn::unexpect_t, Args &&...a) // noexcept(::std::is_nothrow_constructible_v) // @@ -1622,6 +1724,9 @@ template class expected : private detail::_expected_ba : _base(::fn::unexpect, FWD(a)...) { } + /** + * @brief Constructs the error in place from the arguments + */ template constexpr explicit expected(::fn::unexpect_t, ::std::initializer_list il, Args &&...a) // noexcept(::std::is_nothrow_constructible_v &, Args...>) @@ -1630,6 +1735,9 @@ template class expected : private detail::_expected_ba { } + /** + * @brief Copy constructor; not available on this carrier + */ constexpr expected(expected const &) = delete; constexpr expected(expected const &) requires(::std::is_copy_constructible_v && ::std::is_trivially_copy_constructible_v) @@ -1640,6 +1748,9 @@ template class expected : private detail::_expected_ba : _base(s.set_, FWD(s).storage_) { } + /** + * @brief Move constructor + */ constexpr expected(expected &&s) noexcept requires(::std::is_move_constructible_v && ::std::is_trivially_move_constructible_v) = default; @@ -1650,8 +1761,14 @@ template class expected : private detail::_expected_ba { } + /** + * @brief Destructor + */ constexpr ~expected() = default; + /** + * @brief Assignment from an `unexpected` + */ template constexpr expected &operator=(::fn::unexpected const &s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) @@ -1660,6 +1777,9 @@ template class expected : private detail::_expected_ba this->_assign_unexpected(s); return *this; } + /** + * @brief Assignment from an `unexpected` + */ template constexpr expected &operator=(::fn::unexpected &&s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) @@ -1668,6 +1788,9 @@ template class expected : private detail::_expected_ba this->_assign_unexpected(::std::move(s)); return *this; } + /** + * @brief Copy assignment; not available on this carrier + */ constexpr expected &operator=(expected const &) = delete; constexpr expected &operator=(expected const &) // noexcept(::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v) @@ -1684,6 +1807,9 @@ template class expected : private detail::_expected_ba this->_assign(static_cast<_base const &>(s)); return *this; } + /** + * @brief Move assignment + */ constexpr expected &operator=(expected &&) // noexcept(::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v) requires(::std::is_move_constructible_v && ::std::is_move_assignable_v @@ -1702,6 +1828,9 @@ template class expected : private detail::_expected_ba using _base::emplace; + /** + * @brief Swaps the contents with another `expected` + */ constexpr void swap(expected &rhs) // noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v) requires(::std::is_swappable_v && ::std::is_move_constructible_v) @@ -1717,10 +1846,18 @@ template class expected : private detail::_expected_ba using _base::has_value; using _base::value; - // Elimination over both states, mirroring copack's apply family: the value arm takes no value - // (apply) or the ::std::in_place tag alone (apply_type), the error arm the error unpacked as - // fn::apply would hand it over, after ::fn::unexpect in the tagged form. Bodies delegate to - // _expected_base static helpers. + /** + * @brief Eliminates over both states: the active side routes into the callable + * + * The value arm receives nothing - success carries no value - so it is invoked with the + * trailing arguments alone, and the error arm receives the error as `fn::apply` hands it + * over; the arms must yield one result type. Over an uninhabited error side the value arm + * alone is exhaustive. + * + * @param f Callable with arms for both states; `fn::overload` fuses them + * @param args Additional arguments, appended after the content + * @return The callable's result + */ template [[nodiscard]] constexpr auto apply(F &&f, Args &&...args) & // noexcept(noexcept(_base::_apply(*this, FWD(f), FWD(args)...))) // extension @@ -1750,6 +1887,14 @@ template class expected : private detail::_expected_ba return _base::_apply(::std::move(*this), FWD(f), FWD(args)...); } + /** + * @brief Eliminates over both states, converting the result to `Ret` + * + * @tparam Ret Type the results convert to + * @param f Callable with arms for both states; `fn::overload` fuses them + * @param args Additional arguments, appended after the content + * @return The callable's result, converted to `Ret` + */ template [[nodiscard]] constexpr auto apply_r(F &&f, Args &&...args) & // noexcept(noexcept(_base::template _apply_r(*this, FWD(f), FWD(args)...))) // extension @@ -1779,6 +1924,16 @@ template class expected : private detail::_expected_ba return _base::template _apply_r(::std::move(*this), FWD(f), FWD(args)...); } + /** + * @brief Eliminates over both states, keyed by the constructor tag naming the state + * + * The value arm receives `std::in_place` alone, there being no content to follow it, and the + * error arm receives `fn::unexpect` followed by the error. + * + * @param f Callable with arms for both tagged states + * @param args Additional arguments, appended after the content + * @return The callable's result + */ template [[nodiscard]] constexpr auto apply_type(F &&f, Args &&...args) & // noexcept(noexcept(_base::_apply_type(*this, FWD(f), FWD(args)...))) // extension @@ -1808,6 +1963,15 @@ template class expected : private detail::_expected_ba return _base::_apply_type(::std::move(*this), FWD(f), FWD(args)...); } + /** + * @brief Eliminates over both states, keyed by the constructor tag, converting the result to + * `Ret` + * + * @tparam Ret Type the results convert to + * @param f Callable with arms for both tagged states + * @param args Additional arguments, appended after the content + * @return The callable's result, converted to `Ret` + */ template [[nodiscard]] constexpr auto apply_type_r(F &&f, Args &&...args) & // noexcept(noexcept(_base::template _apply_type_r(*this, FWD(f), FWD(args)...))) // extension @@ -1837,7 +2001,20 @@ template class expected : private detail::_expected_ba return _base::template _apply_type_r(::std::move(*this), FWD(f), FWD(args)...); } - // Monadic operations. Bodies delegate to _expected_base static helpers, which perform copack-widening. + // Bodies delegate to _expected_base static helpers, which perform copack-widening. + + /** + * @brief Binds through the callable, invoked with no arguments, which returns an `expected` + * + * As the standard member, extended by grading: a plain error side admits a callback returning + * the identical error type, or its singular lift `copack` - the opt-in to the graded world - + * while a graded (copack) error side unions the callback's error set into its own. Over the + * uninhabited `copack<>` error side the operand passes through and the callback is neither + * invoked nor instantiated. + * + * @param f Callable taking no arguments, returning an `expected` + * @return The callback's `expected`, its error side widened by the operand's grade + */ template constexpr auto and_then(F &&f) & // noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension @@ -1867,6 +2044,18 @@ template class expected : private detail::_expected_ba return _base::_and_then(::std::move(*this), FWD(f)); } + /** + * @brief Binds the error through the callable, which returns an `expected` + * + * The recovery bind: a successful operand passes through, and the callback maps the error - per + * alternative when graded, exhaustively - into a new `expected`. A callback returning a value + * side leaves this carrier for that one; on a plain error side the callback's error type + * replaces the operand's. Over the uninhabited `copack<>` error side the operation is vacuous: + * nothing is asked of the handler, not even that it be callable. + * + * @param f Callable applied on the error, returning an `expected` + * @return The callback's `expected`, or the operand unchanged where it holds success + */ template constexpr auto or_else(F &&f) & // noexcept(noexcept(_base::_or_else(*this, FWD(f)))) // extension @@ -1896,6 +2085,16 @@ template class expected : private detail::_expected_ba return _base::_or_else(::std::move(*this), FWD(f)); } + /** + * @brief Maps through the callable, invoked with no arguments, staying inside the carrier + * + * The callable's result becomes the new value side, so a callback returning `void` leaves the + * carrier's value side `void` and any other return type gives it one. The error side is + * untouched, and an operand holding an error passes through uninvoked. + * + * @param f Callable taking no arguments + * @return An `expected` over the callable's result type, with the same error side + */ template constexpr auto transform(F &&f) & // noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension @@ -1925,6 +2124,17 @@ template class expected : private detail::_expected_ba return _base::_transform(::std::move(*this), FWD(f)); } + /** + * @brief Maps the error through the callable, staying inside the carrier + * + * As the standard member, extended by grading: over a graded (copack) error side the matching + * is exhaustive, and the branches may collapse diverse alternatives into one type - the grade + * narrows to its singular copack - or into a narrower copack. Over the uninhabited `copack<>` + * error side the mapping is the identity, and the callback is neither invoked nor instantiated. + * + * @param f Callable applied on the error + * @return An `expected` over `void` with the mapped error side + */ template constexpr auto transform_error(F &&f) & // noexcept(noexcept(_base::_transform_error(*this, FWD(f)))) // extension @@ -1954,7 +2164,15 @@ template class expected : private detail::_expected_ba return _base::_transform_error(::std::move(*this), FWD(f)); } - // Convert to graded monad. There is no value to relocate here, so only the error's lift weighs. + /** + * @brief Lifts the error side into its singular copack: `expected` becomes + * `expected>` + * + * The explicit opt-in to the graded world. There is no value to relocate here, so only the + * error's lift weighs; an already-graded error side returns `*this` unchanged. + * + * @return The same carrier with its error side graded + */ constexpr auto copack_error() const & noexcept(::std::is_nothrow_constructible_v, error_type const &> && ::std::is_nothrow_move_constructible_v>) // extension @@ -2021,6 +2239,9 @@ using expected_unit = expected>; // The comparison against a value, at namespace scope for the reason given where its siblings are // declared in pfn: it is the one equality operator constrained on the OTHER operand, and that is // safe only where this operand is deduced. +/** + * @brief Compares an `expected` against a value; a failed `expected` equals nothing + */ template requires(not ::std::is_void_v && not detail::_is_some_expected) constexpr bool operator==(expected const &x, T2 const &v) // @@ -2101,21 +2322,15 @@ template struct _expected_efn final { }; } // namespace detail -/** - * @brief The conjunction of fallible carriers: values multiply into a `pack`, errors sum into a - * `copack` - * - * `a & b` succeeds only if both operands succeed, the values folding into one `pack` - a `void` - * side elides, and a copack value distributes into a copack of packs - while at runtime the error - * side holds the leftmost failing operand's error. Two identical error types stay as they are; - * any other pair sums into their normalized `copack_for`, grading not required of the operands. - * Both operands are fully constructed before the operator runs: an error-selection rule, not - * short-circuiting. An identity-cluster operand contributes its value and no error term. - * - * @param lh Left operand - * @param rh Right operand - * @return An `expected` of the folded value product and the summed error side - */ +// The conjunction of fallible carriers: values multiply into a `pack`, errors sum into a +// `copack` +// +// `a & b` succeeds only if both operands succeed, the values folding into one `pack` - a `void` +// side elides, and a copack value distributes into a copack of packs - while at runtime the error +// side holds the leftmost failing operand's error. Two identical error types stay as they are; +// any other pair sums into their normalized `copack_for`, grading not required of the operands. +// Both operands are fully constructed before the operator runs: an error-selection rule, not +// short-circuiting. An identity-cluster operand contributes its value and no error term. // When any of the sides is expected, we do not produce expected, ...> // Instead just elide void and carry non-void (or elide both voids if that's what we get) template @@ -2434,21 +2649,15 @@ constexpr inline bool _nothrow_disj_error } // namespace detail -/** - * @brief The disjunction of fallible carriers: values sum into a `copack`, errors multiply into a - * `pack` - * - * `a | b` fails only if both operands fail: the leftmost engaged operand's value wins, injected - * into the sum of the value types - a same-type pair stays bare, and a `void` side enters a - * genuine sum as `pack<>` - while the error side is the product of both errors, present only when - * every operand failed, all evidence kept positionally. Both operands are fully constructed - * before the operator runs: a value-selection rule, not a lazy fallback. An identity-cluster - * operand makes the disjunction total, collapsing the result into `just` or `choice`. - * - * @param lh Left operand - * @param rh Right operand - * @return An `expected` of the summed value side and the error product - */ +// The disjunction of fallible carriers: values sum into a `copack`, errors multiply into a +// `pack` +// +// `a | b` fails only if both operands fail: the leftmost engaged operand's value wins, injected +// into the sum of the value types - a same-type pair stays bare, and a `void` side enters a +// genuine sum as `pack<>` - while the error side is the product of both errors, present only when +// every operand failed, all evidence kept positionally. Both operands are fully constructed +// before the operator runs: a value-selection rule, not a lazy fallback. An identity-cluster +// operand makes the disjunction total, collapsing the result into `just` or `choice`. // The disjunction: the value channel is the sum of the value types - a same-type pair stays bare, // as the conjunction's same-error sum does - and the error channel is the product of both errors, // present only when every operand failed, all evidence kept positionally. The leftmost engaged diff --git a/include/fn/fail.hpp b/include/fn/fail.hpp index 5a87c6a2..d84b4b30 100644 --- a/include/fn/fail.hpp +++ b/include/fn/fail.hpp @@ -64,7 +64,7 @@ constexpr inline struct fail_t final { } struct apply; -} fail = {}; +} fail = {}; ///< Fails a value the predicate selects: `x | fail(f)` struct fail_t::apply final { /** diff --git a/include/fn/filter.hpp b/include/fn/filter.hpp index f1544663..c6520c8e 100644 --- a/include/fn/filter.hpp +++ b/include/fn/filter.hpp @@ -79,7 +79,7 @@ constexpr inline struct filter_t final { } struct apply; -} filter = {}; +} filter = {}; ///< Rejects a value the predicate refuses: `x | filter(f)` struct filter_t::apply final { /** diff --git a/include/fn/inspect.hpp b/include/fn/inspect.hpp index c11cbe19..419a7019 100644 --- a/include/fn/inspect.hpp +++ b/include/fn/inspect.hpp @@ -66,7 +66,7 @@ constexpr inline struct inspect_t final { } struct apply; -} inspect = {}; +} inspect = {}; ///< Observes the value in passing: `x | inspect(f)` struct inspect_t::apply final { /** diff --git a/include/fn/inspect_error.hpp b/include/fn/inspect_error.hpp index 3c0aaff4..2a211578 100644 --- a/include/fn/inspect_error.hpp +++ b/include/fn/inspect_error.hpp @@ -54,7 +54,7 @@ constexpr inline struct inspect_error_t final { } struct apply; -} inspect_error = {}; +} inspect_error = {}; ///< Observes the error in passing: `x | inspect_error(f)` struct inspect_error_t::apply final { /** diff --git a/include/fn/just.hpp b/include/fn/just.hpp index bc114484..d19472f4 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -91,20 +91,46 @@ template struct just { static_assert(not detail::_some_in_place_type); static_assert(::std::is_same_v>); + /** + * @brief The type of the value side + */ using value_type = T; + /** + * @brief The payload + */ T v_{}; + /** + * @brief Default constructor + */ constexpr just() = default; + /** + * @brief Copy constructor + */ constexpr just(just const &) = default; + /** + * @brief Move constructor + */ constexpr just(just &&) = default; + /** + * @brief Copy assignment + */ constexpr just &operator=(just const &) = default; + /** + * @brief Move assignment + */ constexpr just &operator=(just &&) = default; + /** + * @brief Destructor + */ constexpr ~just() = default; /** * @brief Constructs the payload from a value * + * Explicit exactly where the conversion to `T` is. + * * @param v Value to initialize the payload from */ template @@ -116,11 +142,6 @@ template struct just { { } - /** - * @brief Constructs the payload from a value which does not implicitly convert to `T` - * - * @param v Value to initialize the payload from - */ template constexpr explicit just(U &&v) // NOSONAR cpp:S6458 the some_just constraint excludes same-kind sources noexcept(::std::is_nothrow_constructible_v) @@ -515,12 +536,27 @@ template struct just { * receives the trailing arguments alone (`apply_type` prepends `std::in_place_type`). */ template <> struct just { + /** + * @brief The type of the value side + */ using value_type = void; + /** + * @brief Default constructor + */ constexpr just() = default; + /** + * @brief Constructs the alternative named by the tag, in place from the arguments + */ constexpr explicit just(::std::in_place_type_t) noexcept {} + /** + * @brief Constructs the value in place from the arguments + */ constexpr explicit just(::std::in_place_t) noexcept {} + /** + * @brief Equality; every `just` compares equal to every other + */ [[nodiscard]] constexpr bool operator==(just const &) const noexcept = default; /** diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 4331da5f..2fe1fa66 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -522,15 +522,33 @@ template class optional : private detail::_optional_base { // NO template friend struct ::fn::detail::_optional_base; public: + /** + * @brief The type of the value side + */ using value_type = T; // [optional.iterators]: mirrors pfn::optional, with fn's own iterator type + /** + * @brief Iterator over the value, if any + */ using iterator = detail::_optional_iterator; + /** + * @brief Const iterator over the value, if any + */ using const_iterator = detail::_optional_iterator; // Constructors. Explicit forwarders to the base mirror pfn::optional. + /** + * @brief Default constructor + */ constexpr optional() noexcept : _base(::std::nullopt) {} + /** + * @brief Constructs the empty state + */ constexpr optional(::std::nullopt_t) noexcept : _base(::std::nullopt) {} // NOSONAR cpp:S1709 implicit per spec + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional const &s) // noexcept(::std::is_nothrow_constructible_v) // extension @@ -538,6 +556,9 @@ template class optional : private detail::_optional_base { // NO : _base(s) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional &&s) // noexcept(::std::is_nothrow_constructible_v) // extension @@ -545,6 +566,9 @@ template class optional : private detail::_optional_base { // NO : _base(::std::move(s)) { } + /** + * @brief Constructs the value from a value + */ template > constexpr explicit(not ::std::is_convertible_v) optional(U &&v) // NOSONAR cpp:S6458 _can_convert excludes self noexcept(::std::is_nothrow_constructible_v) // extension @@ -553,6 +577,9 @@ template class optional : private detail::_optional_base { // NO { } + /** + * @brief Constructs the value in place from the arguments + */ template constexpr explicit optional(::std::in_place_t, Args &&...a) // noexcept(::std::is_nothrow_constructible_v) // extension @@ -560,6 +587,9 @@ template class optional : private detail::_optional_base { // NO : _base(::std::in_place, FWD(a)...) { } + /** + * @brief Constructs the value in place from the arguments + */ template constexpr explicit optional(::std::in_place_t, ::std::initializer_list il, Args &&...a) // noexcept(::std::is_nothrow_constructible_v &, Args...>) // extension @@ -568,6 +598,9 @@ template class optional : private detail::_optional_base { // NO { } + /** + * @brief Copy constructor; not available on this carrier + */ constexpr optional(optional const &) = delete; constexpr optional(optional const &s) // noexcept(::std::is_nothrow_copy_constructible_v) // extension @@ -579,6 +612,9 @@ template class optional : private detail::_optional_base { // NO : _base(s.set_, FWD(s).storage_) { } + /** + * @brief Move constructor + */ constexpr optional(optional &&) noexcept requires(::std::is_move_constructible_v && ::std::is_trivially_move_constructible_v) = default; @@ -589,14 +625,23 @@ template class optional : private detail::_optional_base { // NO { } + /** + * @brief Destructor + */ constexpr ~optional() = default; // Assignment. Explicit forwarders to the base mirror pfn::optional. + /** + * @brief Assigns the empty state + */ constexpr optional &operator=(::std::nullopt_t) noexcept { this->reset(); return *this; } + /** + * @brief Copy assignment; not available on this carrier + */ constexpr optional &operator=(optional const &) = delete; constexpr optional &operator=(optional const &) // noexcept(::std::is_nothrow_copy_assignable_v && ::std::is_nothrow_copy_constructible_v) // extension @@ -613,6 +658,9 @@ template class optional : private detail::_optional_base { // NO this->_assign(static_cast<_base const &>(s)); return *this; } + /** + * @brief Move assignment + */ constexpr optional &operator=(optional &&) // noexcept(::std::is_nothrow_move_assignable_v && ::std::is_nothrow_move_constructible_v) requires(::std::is_move_constructible_v && ::std::is_move_assignable_v @@ -629,6 +677,9 @@ template class optional : private detail::_optional_base { // NO return *this; } + /** + * @brief Assignment from a value + */ template > constexpr optional &operator=(U &&v) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) // extension @@ -637,6 +688,9 @@ template class optional : private detail::_optional_base { // NO this->_assign_value(FWD(v)); return *this; } + /** + * @brief Assignment from a compatible carrier + */ template constexpr optional &operator=(optional const &s) // noexcept(::std::is_nothrow_assignable_v @@ -646,6 +700,9 @@ template class optional : private detail::_optional_base { // NO this->_assign_from(s); return *this; } + /** + * @brief Assignment from a compatible carrier + */ template constexpr optional &operator=(optional &&s) // noexcept(::std::is_nothrow_assignable_v && ::std::is_nothrow_constructible_v) // extension @@ -660,6 +717,9 @@ template class optional : private detail::_optional_base { // NO using _base::reset; // Swap; body delegates to _optional_base helper + /** + * @brief Swaps the contents with another `optional` + */ constexpr void swap(optional &rhs) // noexcept(::std::is_nothrow_move_constructible_v && ::std::is_nothrow_swappable_v) { @@ -1034,15 +1094,33 @@ template class optional : private detail::_optional_base { template friend struct ::fn::detail::_optional_base; public: + /** + * @brief The type of the value side + */ using value_type = T; // [optional.ref.iterators]: mirrors pfn::optional, with fn's own iterator type + /** + * @brief Iterator over the value, if any + */ using iterator = detail::_optional_iterator; // Constructors. Explicit forwarders to the base mirror pfn::optional. + /** + * @brief Default constructor + */ constexpr optional() noexcept = default; + /** + * @brief Constructs the empty state + */ constexpr optional(::std::nullopt_t) noexcept : optional() {} // NOSONAR cpp:S1709 implicit per spec + /** + * @brief Copy constructor + */ constexpr optional(optional const &rhs) noexcept = default; + /** + * @brief Constructs the value in place from the arguments + */ template constexpr explicit optional(::std::in_place_t, Arg &&arg) // noexcept(::std::is_nothrow_constructible_v) // extension @@ -1053,12 +1131,18 @@ template class optional : private detail::_optional_base { template constexpr explicit(not ::std::is_convertible_v) + /** + * @brief Constructs the value from a value + */ optional(U &&u) // NOSONAR cpp:S6458 _can_convert excludes self noexcept(::std::is_nothrow_constructible_v) requires(_base::template _can_convert::value) : _base(::std::in_place, FWD(u)) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional &rhs) // noexcept(::std::is_nothrow_constructible_v) @@ -1066,6 +1150,9 @@ template class optional : private detail::_optional_base { : _base(rhs) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional const &rhs) // noexcept(::std::is_nothrow_constructible_v) @@ -1073,6 +1160,9 @@ template class optional : private detail::_optional_base { : _base(rhs) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional &&rhs) // noexcept(::std::is_nothrow_constructible_v) @@ -1080,6 +1170,9 @@ template class optional : private detail::_optional_base { : _base(::std::move(rhs)) { } + /** + * @brief Converting constructor from a compatible carrier + */ template constexpr explicit(not ::std::is_convertible_v) optional(optional const &&rhs) // noexcept(::std::is_nothrow_constructible_v) @@ -1088,14 +1181,23 @@ template class optional : private detail::_optional_base { { } + /** + * @brief Destructor + */ constexpr ~optional() = default; // Assignment + /** + * @brief Assigns the empty state + */ constexpr optional &operator=(::std::nullopt_t) noexcept { this->reset(); return *this; } + /** + * @brief Copy assignment + */ constexpr optional &operator=(optional const &rhs) noexcept = default; // Emplace and reset inherited from _optional_base @@ -1103,6 +1205,9 @@ template class optional : private detail::_optional_base { using _base::reset; // Swap; body delegates to _optional_base helper + /** + * @brief Swaps the contents with another `optional` + */ constexpr void swap(optional &rhs) noexcept { this->_swap_with(rhs); } // Iterator support inherited from _optional_base, mirrors pfn::optional @@ -1117,8 +1222,20 @@ template class optional : private detail::_optional_base { using _base::value; using _base::value_or; - // Elimination over both states; a reference optional always hands the referent over as T&. // Bodies delegate to _optional_base static helpers. + + /** + * @brief Eliminates over both states: the engaged arm receives the referent, the empty arm + * nothing + * + * The engaged arm receives a plain `T&` - a reference optional hands the referent over as + * itself, never by elements - and the empty arm the trailing arguments alone; the arms must + * yield one result type. + * + * @param f Callable with arms for both states; `fn::overload` fuses them + * @param args Additional arguments, appended after the content + * @return The callable's result + */ template [[nodiscard]] constexpr auto apply(F &&f, Args &&...args) const // noexcept(noexcept(_base::_apply(*this, FWD(f), FWD(args)...))) // extension @@ -1126,6 +1243,15 @@ template class optional : private detail::_optional_base { { return _base::_apply(*this, FWD(f), FWD(args)...); } + + /** + * @brief Eliminates over both states, converting the result to `Ret` + * + * @tparam Ret Type the results convert to + * @param f Callable with arms for both states; `fn::overload` fuses them + * @param args Additional arguments, appended after the content + * @return The callable's result, converted to `Ret` + */ template [[nodiscard]] constexpr auto apply_r(F &&f, Args &&...args) const // noexcept(noexcept(_base::template _apply_r(*this, FWD(f), FWD(args)...))) // extension @@ -1133,6 +1259,17 @@ template class optional : private detail::_optional_base { { return _base::template _apply_r(*this, FWD(f), FWD(args)...); } + + /** + * @brief Eliminates over both states, keyed by the tag naming the state + * + * The engaged arm receives `std::in_place` followed by the referent, and the empty arm + * `std::nullopt`. + * + * @param f Callable with arms for both tagged states + * @param args Additional arguments, appended after the content + * @return The callable's result + */ template [[nodiscard]] constexpr auto apply_type(F &&f, Args &&...args) const // noexcept(noexcept(_base::_apply_type(*this, FWD(f), FWD(args)...))) // extension @@ -1140,6 +1277,15 @@ template class optional : private detail::_optional_base { { return _base::_apply_type(*this, FWD(f), FWD(args)...); } + + /** + * @brief Eliminates over both states, keyed by the tag, converting the result to `Ret` + * + * @tparam Ret Type the results convert to + * @param f Callable with arms for both tagged states + * @param args Additional arguments, appended after the content + * @return The callable's result, converted to `Ret` + */ template [[nodiscard]] constexpr auto apply_type_r(F &&f, Args &&...args) const // noexcept(noexcept(_base::template _apply_type_r(*this, FWD(f), FWD(args)...))) // extension @@ -1148,7 +1294,17 @@ template class optional : private detail::_optional_base { return _base::template _apply_type_r(*this, FWD(f), FWD(args)...); } - // Monadic operations. Bodies delegate to _optional_base static helpers. + // Bodies delegate to _optional_base static helpers. + + /** + * @brief Binds the referent through the callable, which returns an `optional` + * + * The callable receives a plain `T&` and names the result outright, so a bind may leave the + * reference behind for an owning `optional`. An empty operand passes through. + * + * @param f Callable applied on the referent, returning an `optional` + * @return The callback's `optional` + */ template constexpr auto and_then(F &&f) const // noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension @@ -1156,6 +1312,17 @@ template class optional : private detail::_optional_base { { return _base::_and_then(*this, FWD(f)); } + + /** + * @brief Maps the referent through the callable, staying inside the carrier + * + * The callable receives a plain `T&`, and its result type becomes the new value side - a + * callable returning a reference keeps the result a view, one returning a value makes it own. + * An empty operand passes through uninvoked. + * + * @param f Callable applied on the referent + * @return An `optional` holding the callable's result + */ template constexpr auto transform(F &&f) const // noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension @@ -1163,6 +1330,18 @@ template class optional : private detail::_optional_base { { return _base::_transform(*this, FWD(f)); } + + /** + * @brief Binds the empty state through the callable, which returns this same `optional` + * + * The recovery bind: an engaged operand passes through, and the callback - invoked with no + * arguments, the empty state carrying no value - supplies the result. A reference optional has + * no value side to grade, so the callback must return this very type, as the standard member + * requires. + * + * @param f Callable invoked with no arguments, returning an `optional` + * @return The recovery's `optional`, or the operand where it is engaged + */ template constexpr auto or_else(F &&f) const // noexcept(noexcept(_base::_or_else_ref(*this, FWD(f)))) // extension @@ -1196,6 +1375,9 @@ concept _is_derived_from_optional = requires(T const &t) { ::fn::detail::_derive // do not apply here, since fn::optional does not derive from pfn::optional). // Relational operators +/** + * @brief Compares two optionals; two empty optionals are equal + */ template constexpr bool operator==(optional const &x, optional const &y) // noexcept(::pfn::detail::_eq_bool_noexcept) // extension @@ -1207,6 +1389,9 @@ constexpr bool operator==(optional const &x, optional const &y) // return true; return *x == *y; } +/** + * @brief The negation of `==` for two optionals + */ template constexpr bool operator!=(optional const &x, optional const &y) // noexcept(::pfn::detail::_ne_bool_noexcept) // extension @@ -1218,6 +1403,9 @@ constexpr bool operator!=(optional const &x, optional const &y) // return false; return *x != *y; } +/** + * @brief Orders two optionals, the empty state before every value + */ template constexpr bool operator<(optional const &x, optional const &y) // noexcept(::pfn::detail::_lt_bool_noexcept) // extension @@ -1229,6 +1417,9 @@ constexpr bool operator<(optional const &x, optional const &y) // return true; return *x < *y; } +/** + * @brief Orders two optionals, the empty state before every value + */ template constexpr bool operator>(optional const &x, optional const &y) // noexcept(::pfn::detail::_gt_bool_noexcept) // extension @@ -1240,6 +1431,9 @@ constexpr bool operator>(optional const &x, optional const &y) // return true; return *x > *y; } +/** + * @brief Orders two optionals, the empty state before every value + */ template constexpr bool operator<=(optional const &x, optional const &y) // noexcept(::pfn::detail::_le_bool_noexcept) // extension @@ -1251,6 +1445,9 @@ constexpr bool operator<=(optional const &x, optional const &y) // return false; return *x <= *y; } +/** + * @brief Orders two optionals, the empty state before every value + */ template constexpr bool operator>=(optional const &x, optional const &y) // noexcept(::pfn::detail::_ge_bool_noexcept) // extension @@ -1262,6 +1459,9 @@ constexpr bool operator>=(optional const &x, optional const &y) // return false; return *x >= *y; } +/** + * @brief Orders two optionals, the empty state before every value + */ template U> constexpr ::std::compare_three_way_result_t operator<=>(optional const &x, optional const &y) { @@ -1269,16 +1469,25 @@ constexpr ::std::compare_three_way_result_t operator<=>(optional const } // Comparison with nullopt +/** + * @brief Whether the optional is empty + */ template constexpr bool operator==(optional const &x, ::std::nullopt_t) noexcept { return not x.has_value(); } +/** + * @brief Orders the optional against the empty state, which precedes every value + */ template constexpr ::std::strong_ordering operator<=>(optional const &x, ::std::nullopt_t) noexcept { return x.has_value() <=> false; } // Comparison with a value +/** + * @brief Compares an optional against a value; an empty optional equals nothing + */ template constexpr bool operator==(optional const &x, U const &v) // noexcept(::pfn::detail::_eq_bool_noexcept) // extension @@ -1286,6 +1495,9 @@ constexpr bool operator==(optional const &x, U const &v) // { return x.has_value() ? *x == v : false; } +/** + * @brief Compares a value against an optional; an empty optional equals nothing + */ template constexpr bool operator==(T const &v, optional const &x) // noexcept(::pfn::detail::_eq_bool_noexcept) // extension @@ -1293,6 +1505,9 @@ constexpr bool operator==(T const &v, optional const &x) // { return x.has_value() ? v == *x : false; } +/** + * @brief The negation of `==` against a value + */ template constexpr bool operator!=(optional const &x, U const &v) // noexcept(::pfn::detail::_ne_bool_noexcept) // extension @@ -1300,6 +1515,9 @@ constexpr bool operator!=(optional const &x, U const &v) // { return x.has_value() ? *x != v : true; } +/** + * @brief The negation of `==` against a value + */ template constexpr bool operator!=(T const &v, optional const &x) // noexcept(::pfn::detail::_ne_bool_noexcept) // extension @@ -1307,6 +1525,9 @@ constexpr bool operator!=(T const &v, optional const &x) // { return x.has_value() ? v != *x : true; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator<(optional const &x, U const &v) // noexcept(::pfn::detail::_lt_bool_noexcept) // extension @@ -1314,6 +1535,9 @@ constexpr bool operator<(optional const &x, U const &v) // { return x.has_value() ? *x < v : true; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator<(T const &v, optional const &x) // noexcept(::pfn::detail::_lt_bool_noexcept) // extension @@ -1321,6 +1545,9 @@ constexpr bool operator<(T const &v, optional const &x) // { return x.has_value() ? v < *x : false; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator>(optional const &x, U const &v) // noexcept(::pfn::detail::_gt_bool_noexcept) // extension @@ -1328,6 +1555,9 @@ constexpr bool operator>(optional const &x, U const &v) // { return x.has_value() ? *x > v : false; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator>(T const &v, optional const &x) // noexcept(::pfn::detail::_gt_bool_noexcept) // extension @@ -1335,6 +1565,9 @@ constexpr bool operator>(T const &v, optional const &x) // { return x.has_value() ? v > *x : true; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator<=(optional const &x, U const &v) // noexcept(::pfn::detail::_le_bool_noexcept) // extension @@ -1342,6 +1575,9 @@ constexpr bool operator<=(optional const &x, U const &v) // { return x.has_value() ? *x <= v : true; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator<=(T const &v, optional const &x) // noexcept(::pfn::detail::_le_bool_noexcept) // extension @@ -1349,6 +1585,9 @@ constexpr bool operator<=(T const &v, optional const &x) // { return x.has_value() ? v <= *x : false; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator>=(optional const &x, U const &v) // noexcept(::pfn::detail::_ge_bool_noexcept) // extension @@ -1356,6 +1595,9 @@ constexpr bool operator>=(optional const &x, U const &v) // { return x.has_value() ? *x >= v : false; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template constexpr bool operator>=(T const &v, optional const &x) // noexcept(::pfn::detail::_ge_bool_noexcept) // extension @@ -1363,6 +1605,9 @@ constexpr bool operator>=(T const &v, optional const &x) // { return x.has_value() ? v >= *x : true; } +/** + * @brief Orders an optional against a value, an empty optional before it + */ template requires(not detail::_is_derived_from_optional) && ::std::three_way_comparable_with constexpr ::std::compare_three_way_result_t operator<=>(optional const &x, U const &v) @@ -1371,6 +1616,9 @@ constexpr ::std::compare_three_way_result_t operator<=>(optional const } // Specialized algorithms +/** + * @brief Swaps two optionals + */ template constexpr void swap(optional &x, optional &y) noexcept(noexcept(x.swap(y))) requires(::std::is_reference_v || (::std::is_move_constructible_v && ::std::is_swappable_v)) @@ -1416,18 +1664,12 @@ struct _optional_efn final { }; } // namespace detail -/** - * @brief The conjunction of optionals: values multiply into a `pack`, empty is the one failure - * - * `a & b` is engaged only if both operands are, the values folding into one `pack` - a copack - * value distributing into a copack of packs - and empty otherwise: `optional`'s unit error needs - * no summing. Both operands are fully constructed before the operator runs. An identity-cluster - * operand contributes its value and can never be the empty side. - * - * @param lh Left operand - * @param rh Right operand - * @return An `optional` of the folded value product - */ +// The conjunction of optionals: values multiply into a `pack`, empty is the one failure +// +// `a & b` is engaged only if both operands are, the values folding into one `pack` - a copack +// value distributing into a copack of packs - and empty otherwise: `optional`'s unit error needs +// no summing. Both operands are fully constructed before the operator runs. An identity-cluster +// operand contributes its value and can never be the empty side. template [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(noexcept(::fn::detail::_join(FWD(lh), FWD(rh), detail::_optional_efn{}))) @@ -1547,18 +1789,12 @@ template return ::std::remove_cvref_t{FWD(lh)}; } -/** - * @brief The disjunction of optionals: values sum into a `copack`, empty only when both are - * - * `a | b` holds the leftmost engaged operand's value, injected into the sum of the value types - - * a same-type pair stays bare. The unit errors vanish in the error product, so the result is - * empty exactly when both operands are. Both operands are fully constructed before the operator - * runs: a value-selection rule, not a lazy fallback. - * - * @param lh Left operand - * @param rh Right operand - * @return An `optional` of the summed value side - */ +// The disjunction of optionals: values sum into a `copack`, empty only when both are +// +// `a | b` holds the leftmost engaged operand's value, injected into the sum of the value types - +// a same-type pair stays bare. The unit errors vanish in the error product, so the result is +// empty exactly when both operands are. Both operands are fully constructed before the operator +// runs: a value-selection rule, not a lazy fallback. // The disjunction: the value channel is the sum of the value types - a same-type pair stays bare - // and the unit errors vanish in the product, so the result is empty exactly when both operands // are. The leftmost engaged operand wins and injects by type. diff --git a/include/fn/or_else.hpp b/include/fn/or_else.hpp index 6d4f88f2..f4149a2a 100644 --- a/include/fn/or_else.hpp +++ b/include/fn/or_else.hpp @@ -173,7 +173,7 @@ constexpr inline struct or_else_t final { } struct apply; -} or_else = {}; +} or_else = {}; ///< Binds the dead state, returning a carrier: `x | or_else(f)` struct or_else_t::apply final { /** diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index 2f981478..442c5474 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -45,6 +45,9 @@ template struct pack : detail::pack_impl<::std::index_sequence_ using _impl = detail::pack_impl<::std::index_sequence_for, Ts...>; static_assert((... && detail::_is_valid_pack_element)); + /** + * @brief The pack type that appending a `T` yields + */ template using append_type = _impl::template append_type; /** @@ -62,6 +65,9 @@ template struct pack : detail::pack_impl<::std::index_sequence_ return _impl::_equal(*this, other); } + /** + * @brief Orders two packs lexicographically, element by element + */ [[nodiscard]] constexpr auto operator<=>(pack const &other) const // noexcept(noexcept(_impl::_compare(*this, other))) requires requires(pack const &a, pack const &b) { _impl::_compare(a, b); } @@ -461,10 +467,15 @@ constexpr inline struct conjoin_t { template [[nodiscard]] constexpr auto operator()(Arg &&arg) const -> decltype(arg) { return FWD(arg); } /** - * @brief Folds data into a product, lifting the leading scalar into a `pack` first - * @param arg The leading scalar - * @param args Data to conjoin - scalars, packs or copacks, never carriers - * @return The folded product + * @brief Folds data into a product, or carriers into their conjunction + * + * With no carrier among the arguments the fold is the data-level product: a leading scalar is + * lifted into a `pack` first, and a leading `pack` or `copack` dispatches `operator &` itself. + * With every argument a carrier the same fold is their monadic conjunction. + * + * @param arg The leading argument + * @param args Further arguments - all data, or all carriers, never the two mixed + * @return The folded product, or the folded conjunction */ template requires(not some_copack) && (not some_pack) && detail::_no_carrier @@ -473,12 +484,6 @@ constexpr inline struct conjoin_t { return (::fn::pack{FWD(arg)} & ... & FWD(args)); } - /** - * @brief Folds data into a product, the leading `pack` or `copack` dispatching `operator &` - * @param arg The leading pack or copack - * @param args Data to conjoin - scalars, packs or copacks, never carriers - * @return The folded product - */ template requires(some_copack || some_pack) && detail::_no_carrier [[nodiscard]] constexpr auto operator()(Arg &&arg, Args &&...args) const @@ -486,13 +491,6 @@ constexpr inline struct conjoin_t { return (FWD(arg) & ... & FWD(args)); } - /** - * @brief The same fold over carriers, where `operator &` is the conjunction of the carriers - * - * @param arg The leading carrier - * @param args Carriers to conjoin - * @return The folded conjunction - */ template requires(sizeof...(Args) > 0) && detail::_all_carriers && requires(Arg &&a, Args &&...as) { (FWD(a) & ... & FWD(as)); } @@ -501,7 +499,7 @@ constexpr inline struct conjoin_t { { return (FWD(arg) & ... & FWD(args)); } -} conjoin; +} conjoin; ///< The n-ary conjunction: `conjoin(a, b, c)` /** * @brief The n-ary fold of the disjunction `operator |` over the monadic carriers; a single @@ -513,11 +511,28 @@ constexpr inline struct conjoin_t { constexpr inline struct disjoin_t { // Carriers only, in every arity: `|` over anything else is the built-in operator, and folding // integers into 3 is not what this asks for + + /** + * @brief Forwards a single carrier unchanged + * @param arg The carrier + * @return The carrier, forwarded + */ template [[nodiscard]] constexpr auto operator()(Arg &&arg) const -> decltype(arg) { return FWD(arg); } + /** + * @brief Folds the carriers into their disjunction + * + * The n-ary form of `operator |`: the result holds the first operand that worked, its values + * summing into a `copack`, and the errors multiply into a `pack` reached only where every + * operand failed. An identity-cluster operand makes the whole disjunction total. + * + * @param arg The leading carrier + * @param args Further carriers to disjoin + * @return The folded disjunction + */ template requires(sizeof...(Args) > 0) && detail::_all_carriers && requires(Arg &&a, Args &&...as) { (FWD(a) | ... | FWD(as)); } @@ -526,7 +541,7 @@ constexpr inline struct disjoin_t { { return (FWD(arg) | ... | FWD(args)); } -} disjoin; +} disjoin; ///< The n-ary disjunction: `disjoin(a, b, c)` } // namespace LIBFN_VERSION } // namespace fn diff --git a/include/fn/recover.hpp b/include/fn/recover.hpp index 938c4eba..fe07badf 100644 --- a/include/fn/recover.hpp +++ b/include/fn/recover.hpp @@ -71,7 +71,7 @@ constexpr inline struct recover_t final { } struct apply; -} recover = {}; +} recover = {}; ///< Supplies a value for the dead state: `x | recover(f)` struct recover_t::apply final { /** diff --git a/include/fn/transform.hpp b/include/fn/transform.hpp index fc5f8d23..8154ac73 100644 --- a/include/fn/transform.hpp +++ b/include/fn/transform.hpp @@ -117,7 +117,7 @@ constexpr inline struct transform_t final { } struct apply; -} transform = {}; +} transform = {}; ///< Maps the value, staying in the carrier: `x | transform(f)` struct transform_t::apply final { /** diff --git a/include/fn/transform_error.hpp b/include/fn/transform_error.hpp index c47f828e..74470bf8 100644 --- a/include/fn/transform_error.hpp +++ b/include/fn/transform_error.hpp @@ -56,7 +56,7 @@ constexpr inline struct transform_error_t final { } struct apply; -} transform_error = {}; +} transform_error = {}; ///< Maps the error, staying in the carrier: `x | transform_error(f)` struct transform_error_t::apply final { /** diff --git a/include/fn/utility.hpp b/include/fn/utility.hpp index d123014f..0dcf6b79 100644 --- a/include/fn/utility.hpp +++ b/include/fn/utility.hpp @@ -57,7 +57,10 @@ template struct overload final : Ts... { template overload(Ts const &...) -> overload; /** - * @brief Preferred make lift function using {} + * @brief Lifts arguments into a `T`, preferring braced construction + * + * Constructs `T{args...}` where that is available and `T(args...)` where it is not, so a type + * whose braced form differs from its parenthesised one is reached the way its author meant. * * @tparam T Type to construct * @param args Arguments to construct the `T` from @@ -70,13 +73,6 @@ template return T{FWD(args)...}; } -/** - * @brief Fallback to () construction if {} is not available - * - * @tparam T Type to construct - * @param args Arguments to construct the `T` from - * @return The constructed value - */ template [[nodiscard]] constexpr auto make(Args &&...args) -> T requires requires(Args &&...args) { T(FWD(args)...); } && (not requires(Args &&...args) { T{FWD(args)...}; }) diff --git a/include/fn/value_or.hpp b/include/fn/value_or.hpp index f066e2bd..0bc1adea 100644 --- a/include/fn/value_or.hpp +++ b/include/fn/value_or.hpp @@ -61,7 +61,7 @@ constexpr inline struct value_or_t final { } struct apply; -} value_or = {}; +} value_or = {}; ///< Substitutes a fallback for the dead state: `x | value_or(v)` struct value_or_t::apply final { /** diff --git a/include/pfn/expected.hpp b/include/pfn/expected.hpp index ff7f5da9..a88028fa 100644 --- a/include/pfn/expected.hpp +++ b/include/pfn/expected.hpp @@ -57,6 +57,9 @@ template class bad_expected_access; * * Thrown only as a `bad_expected_access`; this specialization for `void` is the common base, * for handlers that do not care about the error type. + * + * Its members are the standard's, and specified where + * the reference page for this entity points. */ template <> class bad_expected_access : public ::std::exception { protected: @@ -81,6 +84,9 @@ template <> class bad_expected_access : public ::std::exception { * * Carries a copy of that error, exposed through `error()`. * + * + * Its members are the standard's, and specified where + * the reference page for this entity points. * @tparam E Type of the carried error value */ template class bad_expected_access : public bad_expected_access { @@ -99,6 +105,9 @@ template class bad_expected_access : public bad_expected_access /** * @brief Disambiguation tag selecting an `expected`'s error side in construction and emplacement * ([expected.syn]); passed as the `pfn::unexpect` value + * + * Its members are the standard's, and specified where + * the reference page for this entity points. */ constexpr inline struct unexpect_t { explicit unexpect_t() = default; @@ -145,6 +154,9 @@ constexpr inline struct _expected_from_invoke_t { * Deviation: `operator==` carries a `noexcept` specification derived from `E` where the standard * leaves one unstated, marked `// extension` inline. * + * + * Its members are the standard's, and specified where + * the reference page for this entity points. * @tparam E Type of the wrapped error value */ template class unexpected { @@ -1159,6 +1171,9 @@ struct expected_policy { * - the comparison against a value is declared at namespace scope, not as the specified hidden * friend, keeping its constraint deducible. * + * + * Its members are the standard's, and specified where + * the reference page for this entity points. * @tparam T Type of the success value; `void` selects the specialization * @tparam E Type of the error value */ @@ -1525,6 +1540,9 @@ template class expected : private detail::_expected_base class expected : private detail::_expected_base { diff --git a/include/pfn/optional.hpp b/include/pfn/optional.hpp index 34bbda22..a026d987 100644 --- a/include/pfn/optional.hpp +++ b/include/pfn/optional.hpp @@ -1053,6 +1053,9 @@ template struct _optional_hash_base { * - the draft's hardened preconditions are checked by an assertion, customizable by defining * `LIBFN_ASSERT` before inclusion. * + * + * Its members are the standard's, and specified where + * the reference page for this entity points. * @tparam T Type of the contained value; an lvalue reference selects the specialization */ template class optional : private detail::_optional_base { @@ -1322,6 +1325,9 @@ template optional(T) -> optional; * trait with no portable C++20 fallback - those guards are deferred, and such a construction * compiles and dangles. * + * + * Its members are the standard's, and specified where + * the reference page for this entity points. * @tparam T Referenced type */ template class optional : private detail::_optional_base { diff --git a/scripts/check_docs_coverage.py b/scripts/check_docs_coverage.py new file mode 100644 index 00000000..94193457 --- /dev/null +++ b/scripts/check_docs_coverage.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Check that what is documented reaches the documentation. + +Three ways a description silently fails to arrive, each of which has happened here: + + unpublished an entity carries a doxygen stanza that no page names, so the words exist and + the site never shows them - forty-eight carrier members were in this state + empty a page asks for a compound that carries no description, and znai renders + silence rather than complaining - fn::choice's page opened with nothing + unrendered a page asks for an overload set whose first member is the undocumented one, + and znai renders the first member's description, which is empty - operator& + and operator| were in this state + +Entities deliberately left undocumented are listed in the exemption file, with a reason, so +that the omission is a decision on the record and anything new fails instead. +""" + +import argparse +import pathlib +import re +import sys +import xml.etree.ElementTree as ET + +# the library marks what is not its interface with a leading underscore, and keeps its +# implementation in `detail` +INTERNAL = re.compile(r"(^|::)(detail|_impl)(::|$)|(^|::)_") +DOC_DIRECTIVE = re.compile(r"^:include-doxygen-doc:[ \t]+([^{\n]+?)(?:[ \t]*\{(.*)\})?[ \t]*$", + re.M) +ARGS_OPT = re.compile(r"""\bargs\s*:\s*"([^"]*)\"""") + + +def described(node): + for tag in ("briefdescription", "detaileddescription"): + child = node.find(tag) + if child is not None and "".join(child.itertext()).strip(): + return True + return False + + +def user_facing(name): + return bool(name) and name.split("::")[0] in ("fn", "pfn") and not INTERNAL.search(name) + + +class Api: + """Every user-facing entity doxygen found, and how it is documented.""" + + def __init__(self, xml_dir): + self.compounds = {} # name -> described + self.members = {} # name -> [(selector, described)] in doxygen order + for path in sorted(pathlib.Path(xml_dir).glob("*.xml")): + if path.name == "index.xml": + continue + for compound in ET.parse(path).getroot().iter("compounddef"): + cname = compound.findtext("compoundname") or "" + if compound.get("kind") not in ("file", "dir") and user_facing(cname): + self.compounds[cname] = described(compound) + for node in compound.iter("memberdef"): + if node.get("prot") not in (None, "public") or node.get("kind") == "friend": + continue + qualified = node.findtext("qualifiedname") or "" + if not user_facing(qualified): + continue + types = ["".join(p.find("type").itertext()).replace(" &", "&") + for p in node.findall("param") if p.find("type") is not None] + self.members.setdefault(qualified, []).append( + (",".join(types), described(node))) + + def documented(self): + for name, flag in self.compounds.items(): + if flag: + yield name + for name, overloads in self.members.items(): + if any(flag for _, flag in overloads): + yield name + + +def load_exemptions(path): + allowed = {} + if not path.is_file(): + return allowed + for number, line in enumerate(path.read_text().splitlines(), 1): + line = line.split("#", 1)[0].strip() if not line.lstrip().startswith("#") else "" + if not line: + continue + # a colon FOLLOWED BY A SPACE separates the two: a qualified name is full of `::` + if ": " not in line: + raise SystemExit(f"{path}:{number}: expected ': '") + name, reason = line.split(": ", 1) + if not reason.strip(): + raise SystemExit(f"{path}:{number}: {name.strip()} needs a reason") + allowed[name.strip()] = reason.strip() + return allowed + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--xml", required=True, help="doxygen XML output directory") + parser.add_argument("--docs", required=True, help="the docs/ directory") + args = parser.parse_args() + + api = Api(args.xml) + docs = pathlib.Path(args.docs) + pages = sorted(docs.rglob("*.md")) + text = " ".join(p.read_text() for p in pages) + exempt = load_exemptions(docs / "coverage-exemptions.txt") + + problems, used = [], set() + for name in sorted(api.documented()): + if name in text: + continue + if name in exempt: + used.add(name) + continue + problems.append(f"{name} is documented and no page names it") + + for page in pages: + for name, opts in DOC_DIRECTIVE.findall(page.read_text()): + name = name.strip() + if name in api.compounds: + if not api.compounds[name]: + problems.append(f"{page.name}: {name} carries no description, so the page " + f"renders nothing there") + continue + overloads = api.members.get(name) + if overloads is None: + continue + wanted = ARGS_OPT.search(opts or "") + selector = None + if wanted: + query = re.sub(r",\s+", ",", wanted.group(1).strip()) + selector = re.sub(r"\s+", " ", query).replace(" &", "&").replace(" *", "*") + first = next(((s, d) for s, d in overloads if selector is None or s == selector), None) + if first and not first[1]: + problems.append(f"{page.name}: {name} renders the description of an overload " + f"that has none - the documented one is not the first") + + for name in sorted(set(exempt) - used): + problems.append(f"{name} is exempted but no longer needs to be; drop it from " + f"coverage-exemptions.txt") + + for problem in problems: + print(problem, file=sys.stderr) + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/doxygen_signatures.py b/scripts/doxygen_signatures.py new file mode 100644 index 00000000..5fbbdce7 --- /dev/null +++ b/scripts/doxygen_signatures.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +"""Keep the signature listings in docs/reference in step with the headers. + +A reference page presents an overload set the way cppreference does: the whole set of +signatures in one listing, numbered, then one description per documented overload. znai +cannot draw that listing itself - its doxygen node carries no ref-qualifier, so the four +value-category overloads of a member render as four identical entries - so each listing is +written out in the page, titled with the member it covers, and checked here against the +doxygen XML. + + check - every listing matches the overloads doxygen found + emit - print a section body for a member, to paste into a page + +`check` also reports descriptions that cannot reach the site: znai selects an overload by +its parameter types alone, so two documented overloads differing only by a requires-clause +are indistinguishable to it, and only the first is ever rendered. +""" + +import argparse +import pathlib +import re +import sys +import xml.etree.ElementTree as ET +from collections import defaultdict + +TITLED_FENCE = re.compile( + r"^```cpp[ \t]*\{[^}]*\btitle[ \t]*:[ \t]*\"([^\"]+)\"[^}]*\}[ \t]*\n(.*?)^```[ \t]*$", + re.M | re.S) +ANY_FENCE = re.compile(r"^```cpp\b", re.M) +NUMBER_COMMENT = re.compile(r"//\s*\(\d+\)\s*$") +TRAILING_CONSTRAINT = re.compile(r"\brequires\b.*$", re.S) +# the library marks what is not its interface with a leading underscore +INTERNAL_NAME = re.compile(r"(^|[^A-Za-z0-9_])_[A-Za-z]") + + +def prettify(text): + """Spell types as a reader of the documentation would. + + Headers anchor the standard library as `::std::` so that a user's own `fn::std` cannot + win lookup inside namespace `fn`; that hazard does not exist in prose, and doxygen's + `Type< Arg >` spacing is its own, not the project's. + """ + text = text.replace("::std::", "std::") + text = re.sub(r"<\s+", "<", text) + text = re.sub(r"\s+>", ">", text) + return re.sub(r"\s+", " ", text).strip() + + +def normalize(line): + """Compare listings by content, so a page is free to align its columns.""" + line = NUMBER_COMMENT.sub("", line) + return re.sub(r"\s*([<>(),])\s*", r"\1", prettify(line)).rstrip(";").strip() + + +def xml_text(node): + return " ".join("".join(node.itertext()).split()) if node is not None else "" + + +def linked_text(node): + """A type as znai reads it, so a selector written here is one znai can match. + + znai turns a doxygen `` into parts - one per `` link, one per run of plain + text - then trims each and joins them with a space. A linked type name therefore gains + a space before whatever follows it: `copack < Tx... > const &`, not `copack<...>`. + """ + if node is None: + return "" + parts = [node.text or ""] + for child in node: + parts.append("".join(child.itertext())) + parts.append(child.tail or "") + return " ".join(p.strip() for p in parts if p.strip()) + + +class Member: + """One doxygen memberdef: an overload, and whether it carries documentation.""" + + def __init__(self, node): + self.node = node + self.name = node.findtext("name") + self.qualified = node.findtext("qualifiedname") or "" + self.args = (node.findtext("argsstring") or "").strip() + self.brief = xml_text(node.find("briefdescription")) + self.detail = xml_text(node.find("detaileddescription")) + types = [linked_text(p.find("type")) + for p in node.findall("param") if p.find("type") is not None] + # znai compares parameter types with their spaces collapsed but the ones doxygen + # puts inside angle brackets kept, so a selector must be spelled its way. + self.selector = ",".join(t.replace(" &", "&").replace(" *", "*") for t in types) + self.args_text = ", ".join(types) + + @property + def documented(self): + return bool(self.brief or self.detail) + + @property + def selectable(self): + """Whether znai can be asked for this overload at all. + + It normalizes the query with `,\\s+` -> `,` but leaves the member's own spelling alone, + so a parameter type carrying a comma inside its template arguments - `expected + const &` - is unreachable: no query string normalizes to what the member holds. + """ + query = re.sub(r",\s+", ",", self.args_text.strip()) + query = re.sub(r"\s+", " ", query).replace(" &", "&").replace(" *", "*") + return query == self.selector + + def describes(self, *kinds): + """Whether the description feeds znai an api-parameters table of this kind.""" + detail = self.node.find("detaileddescription") + if detail is None: + return False + found = {p.get("kind") for p in detail.iter("parameterlist")} + found |= {s.get("kind") for s in detail.iter("simplesect")} + return bool(found & set(kinds)) + + @property + def declared_type(self): + """doxygen's , less what belongs elsewhere in the declaration. + + A trailing requires-clause lands here, and a hidden friend carries its `friend` + here rather than among the attributes. + """ + declared = xml_text(self.node.find("type")) + if declared.startswith("friend "): + declared = declared[len("friend "):] + return prettify(TRAILING_CONSTRAINT.sub("", declared)) + + @property + def is_special(self): + """A constructor or destructor, which has no return type to draw.""" + owner = self.qualified.rsplit("::", 1)[0] if "::" in self.qualified else "" + owner = re.sub(r"<[^<>]*>", "", owner).rsplit("::", 1)[-1].strip() + return bool(owner) and self.name.lstrip("~") == owner + + @property + def returns(self): + """The trailing return type to draw, if any. + + A deduced `auto` says nothing a reader can use, an alias carries its type in the + declaration itself, and a SFINAE-friendly `decltype` over the library's internals + names what a reader cannot - all three are described in prose instead, which is + what cppreference does with the same declarations. + """ + if self.is_special or self.node.get("kind") in ("typedef", "variable"): + return "" + if self.declared_type == "auto" or INTERNAL_NAME.search(self.declared_type): + return "" + return self.declared_type + + @property + def template_line(self): + params = self.node.find("templateparamlist") + if params is None: + return None + spelled = [f"{xml_text(p.find('type'))} {xml_text(p.find('declname'))}".strip() + for p in params] + return prettify(f"template <{', '.join(spelled)}>") + + def declaration(self): + """As the headers declare it: trailing return type, no noexcept, no constraint. + + doxygen glues a plain `noexcept` onto argsstring and omits a conditional one; both + are left out for the reason cppreference leaves them out - a listing is for + scanning, and the conditions belong in the prose. + """ + if self.node.get("kind") == "typedef": + return prettify(f"using {self.name} = {self.declared_type}") + if self.node.get("kind") == "variable": + # a variable is its type and name, not a call: no `auto`, no trailing return, and + # the initializer is the point of a constant like `size` + keywords = [k for k, v in (("static", self.node.get("static")), + ("constexpr", self.node.get("constexpr"))) + if v == "yes"] + init = xml_text(self.node.find("initializer")) + # doxygen spells a variable's own type elaborated and fully qualified + # (`struct fn::discard_t`); a reader inside the namespace writes neither + spelled = re.sub(r"^(struct|class|union)\s+", "", self.declared_type) + spelled = re.sub(r"^(::)?p?fn::", "", spelled) + decl = " ".join(keywords + [spelled, self.name]) + return prettify(f"{decl} {init}" if init.startswith("=") else decl) + if not self.is_special and not xml_text(self.node.find("type")): + return prettify(f"{self.name}{self.args}") # a guide keeps its arrow in argsstring + args = re.sub(r"\s*\bnoexcept\b\s*$", "", re.sub(r"\s*->.*$", "", self.args)).strip() + args = re.sub(r"\s*=\s*(default|delete)\b", r" = \1", args) + keywords = [k for k, v in (("static", self.node.get("static")), + ("constexpr", self.node.get("constexpr")), + ("explicit", self.node.get("explicit"))) + if v == "yes"] + if xml_text(self.node.find("type")).startswith("friend "): + keywords.insert(0, "friend") + if self.is_special: + return prettify(f"{' '.join(keywords)} {self.name}{args}") + return prettify(f"{' '.join(keywords + ['auto'])} {self.name}{args}") + + +def load_members(xml_dir): + members = defaultdict(list) + for path in sorted(pathlib.Path(xml_dir).glob("*.xml")): + if path.name == "index.xml": + continue + for compound in ET.parse(path).getroot().iter("compounddef"): + cname = compound.findtext("compoundname") or "" + for node in compound.iter("memberdef"): + member = Member(node) + members[member.qualified or f"{cname}::{member.name}"].append(member) + return members + + +def lookup(name, members): + """Pages name a class template as the headers do; doxygen strips the arguments.""" + if name in members: + return members[name] + stripped = re.sub(r"<[^<>]*>", "", name).replace(" ", "") + for key, value in members.items(): + if key.replace(" ", "") == stripped: + return value + return None + + +def distinct(overloads): + """Overloads a listing can tell apart. + + Two overloads separated only by a requires-clause declare identically, so they earn one + line between them rather than a run of repeats the reader cannot account for. + """ + out = [] + for member in overloads: + shape = (member.template_line, member.declaration(), member.returns) + if not out or out[-1][0] != shape: + out.append((shape, member)) + return [member for _, member in out] + + +def listing(overloads): + """Signatures for one member, grouped by template line and aligned for reading.""" + shown = distinct(overloads) + groups, current = [], [] + for i, member in enumerate(shown): + if current and member.template_line != shown[i - 1].template_line: + groups.append(current) + current = [] + current.append((i + 1, member)) + groups.append(current) + + out = [] + for group in groups: + if out: + out.append("") + if group[0][1].template_line: + out.append(group[0][1].template_line) + decls = [m.declaration() for _, m in group] + arrows = [m.returns for _, m in group] + width = max(len(d) for d in decls) + span = max((len(r) for r in arrows if r), default=0) + for (number, _), decl, ret in zip(group, decls, arrows): + line = f"{decl:<{width}} -> {ret};" if ret else f"{decl};" + pad = width + len(" -> ") + span + 1 if span else width + 1 + out.append(f"{line:<{pad}} // ({number})") + return "\n".join(line.rstrip() for line in out) + + +def selectors(overloads): + """The distinct parameter-type lists znai can select an overload by, in order.""" + groups = {} + for member in overloads: + groups.setdefault(member.selector, []).append(member) + return groups + + +def check(pages, members): + problems = [] + for page in pages: + text = page.read_text() + titled = TITLED_FENCE.findall(text) + if len(ANY_FENCE.findall(text)) != len(titled): + problems.append(f"{page.name}: a cpp listing has no {{title: \"\"}}") + for name, body in titled: + overloads = lookup(name, members) + if overloads is None: + problems.append(f"{page.name}: {name} is not in the doxygen XML") + continue + want = [normalize(x) for x in listing(overloads).splitlines() if x.strip()] + have = [normalize(x) for x in body.splitlines() if x.strip()] + if want == have: + continue + problems.append(f"{page.name}: the listing for {name} does not match the headers") + problems += [f" missing: {x}" for x in want if x not in have] + problems += [f" stale: {x}" for x in have if x not in want] + return problems + + +def unreachable(members_by_page): + """Descriptions that cannot reach the site, split by whether anything can be done. + + Two stanzas on one selector is ours to fix - merge them, as the cv/ref sets are merged. + A selector znai cannot name is not: it is reported so the loss is on the record and so + that a znai release which fixes the normalization is noticed, but it does not fail a + build that no edit here could make pass. + """ + ours, znais = [], [] + for page, name, overloads in members_by_page: + for selector, group in selectors(overloads).items(): + documented = [m for m in group if m.documented] + if len(documented) > 1: + ours.append( + f"{page}: {name} has {len(documented)} descriptions sharing the " + f"selector {selector!r}; znai can render only the first") + elif documented and not group[0].selectable: + znais.append( + f"{page}: {name} is documented for {selector!r}, which no znai selector " + f"can name - its signature is listed, its description is not") + return ours, znais + + +def section(name, overloads): + """A whole section body: the listing, then what znai can select per overload.""" + out = [f'```cpp {{title: "{name}"}}', listing(overloads), "```"] + for group in selectors(overloads).values(): + first = group[0] + if not first.documented or not first.selectable: + continue + out.append("") + out.append(f':include-doxygen-doc: {name} {{ args: "{first.args_text}" }}') + # the standalone params plugin titles nothing of its own, and two untitled tables + # in a row read as one + if first.describes("templateparam"): + out.append("") + out.append(f':include-doxygen-doc-params: {name} {{ args: "{first.args_text}", ' + f'type: "template", title: "template parameters" }}') + if first.describes("param", "return"): + out.append("") + out.append(f':include-doxygen-doc-params: {name} {{ args: "{first.args_text}", ' + f'title: "parameters" }}') + return "\n".join(out) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("check", "emit")) + parser.add_argument("names", nargs="*", help="members to emit a section for") + parser.add_argument("--xml", required=True, help="doxygen XML output directory") + parser.add_argument("--docs", required=True, help="the docs/reference directory") + args = parser.parse_args() + + members = load_members(args.xml) + pages = sorted(pathlib.Path(args.docs).glob("*.md")) + + if args.command == "emit": + for name in args.names: + overloads = lookup(name, members) + if overloads is None: + raise SystemExit(f"{name} is not in the doxygen XML") + print(section(name, overloads)) + return 0 + + named = [(page.name, name, lookup(name, members)) + for page in pages + for name, _ in TITLED_FENCE.findall(page.read_text())] + ours, znais = unreachable([(p, n, o) for p, n, o in named if o is not None]) + for note in znais: + print(f"note: {note}", file=sys.stderr) + problems = check(pages, members) + ours + for problem in problems: + print(problem, file=sys.stderr) + return 1 if problems else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/fix_site_urls.py b/scripts/fix_site_urls.py new file mode 100644 index 00000000..34a4b676 --- /dev/null +++ b/scripts/fix_site_urls.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Repair the links znai writes into the deployed site. + +Two repairs, both for want of anywhere earlier to make them. + +znai builds every internal URL as `///`. The site is deployed at the root +of its domain, so the doc id is empty and the URL arrives doubled, as `//chapter/page` - which a +browser reads as a host name rather than a path, sending the reader off the site entirely. The +navigation escapes this because it is rendered by script that never follows the href; a link +written in a page does not. Collapsing the pair to one slash restores the path znai meant. + +`View on GitHub` is built by the browser as the site's base link followed by the page's own +`viewOnRelativePath`, or by its chapter and file name when that is absent - and absent is all it +ever is, nothing on znai's side populating it. Left alone, a page generated from a root document +would point at a file that does not exist, so each page is given the path it was rendered from. +""" +from __future__ import annotations + +import json +import pathlib +import re +import sys + +import stage_docs_source as staging + +MARKUP = (".html", ".json", ".js") + +# Only where znai spells an internal target: an external one carries its scheme, so `https://` +# is never matched. +DOUBLED = re.compile(r'((?:"url" ?: ?|href=)")//') + +# A serialized toc item, wherever it appears: in the page, in the navigation, in the page index. +TOC_ITEM = re.compile( + r'("dirName"\s*:\s*"([^"]*)",\s*"fileName"\s*:\s*"([^"]*)",\s*' + r'"fileExtension"\s*:\s*"[^"]*",\s*"viewOnRelativePath"\s*:\s*)null') + + +def source_of(chapter: str, page: str) -> str | None: + """The repository file a page was rendered from, or None where there is no one file.""" + for document, directory, _split in staging.DOCUMENTS: + if directory == chapter: + return document + return f"docs/{chapter}/{page}.md" if chapter else None + + +def main() -> None: + if len(sys.argv) != 2: + sys.stderr.write("usage: fix_site_urls.py \n") + sys.exit(2) + + site = pathlib.Path(sys.argv[1]) + if not site.is_dir(): + sys.stderr.write(f"Error: {site} is not a deployed site directory\n") + sys.exit(2) + + def point_at_source(match: re.Match[str]) -> str: + source = source_of(match.group(2), match.group(3)) + return match.group(1) + (json.dumps(source) if source else "null") + + links = pages = 0 + for path in site.rglob("*"): + if path.suffix not in MARKUP or not path.is_file(): + continue + original = path.read_text(encoding="utf-8") + + text, count = DOUBLED.subn(r"\1/", original) + links += count + text, count = TOC_ITEM.subn(point_at_source, text) + pages += count + + if text != original: + path.write_text(text, encoding="utf-8") + + if not links: + sys.stderr.write("Error: no doubled internal links found; has znai stopped doubling them?\n") + sys.exit(2) + if not pages: + sys.stderr.write("Error: no page was pointed at its source; has znai started doing it?\n") + sys.exit(2) + print(f"repaired {links} internal links, pointed {pages} pages at their source") + + +if __name__ == "__main__": + main() diff --git a/scripts/stage_docs_source.py b/scripts/stage_docs_source.py new file mode 100644 index 00000000..89b7ce72 --- /dev/null +++ b/scripts/stage_docs_source.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Stage the znai source tree, giving the site a section per root document. + +Each section is named for the document it carries, so a reader can tell which file of the +repository they are reading; its pages keep the document's own titles. znai renders a directory +as a chapter and each file in it as a page, so a document either stays whole as a single page or +splits at its `##` boundaries. Section numbering stays in the source and never reaches the site: +a page carries the name alone, and the prose's `Section N` cross-references become links to the +pages they name. Repo-relative links, which znai would otherwise resolve as page references and +reject, become links to the sibling section or to the file on GitHub. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import re +import shutil +import sys + +REPO = "https://github.com/libfn/functional" + +# The site's sections in order: a root document, the directory carrying it, and whether its `##` +# sections become pages of their own. README, CONTRIBUTING and LICENSE each read as one piece, so +# they stay whole; TYPE_ALGEBRA is long enough that its sections navigate better as pages. With no +# index.md of its own the site gets a generated one, redirecting the root to the first page below +# — so README needs no second copy to serve as a front page. +HAND_WRITTEN = None # where the chapters docs/toc names are threaded into the order + +SECTIONS = ( + ("README.md", "readme", False), + ("TYPE_ALGEBRA.md", "type-algebra", True), + ("CONTRIBUTING.md", "contributing", False), + HAND_WRITTEN, + ("LICENSE.md", "license", False), +) +DOCUMENTS = tuple(section for section in SECTIONS if section is not HAND_WRITTEN) + +# A line that is nothing but a linked image: a badge, which belongs on the repository page and +# not on the documentation site. A bare image is content and stays. +BADGE = re.compile(r"^\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)$") + +SUBTITLE = re.compile(r"^\*\*([^*]+)\*\*$") +NUMBERED = re.compile(r"^\d+\.\s+") +SECTION_REF = re.compile(r"\bSection (\d+)\b") +INLINE_LINK = re.compile(r"(?<=\])\((?!\()([^()\s]+)((?:\s+\"[^\"]*\")?)\)") +REFERENCE_LINK = re.compile(r"^(\[[^\]]+\]:\s*)(\S+)", re.MULTILINE) +HEADING = re.compile(r"^(#{3,})(?=\s)") +CODE_SPAN = re.compile(r"`+[^`]*`+") + + +def fail(message: str) -> None: + sys.stderr.write(f"Error: {message}\n") + sys.exit(2) + + +def slug(text: str) -> str: + """The anchor GitHub derives from a heading, which is also the page name we give it.""" + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + + +def outside_fences(text: str): + """Yield (index, line, inside_fence) so no rule fires on fenced content.""" + fence = False + for index, line in enumerate(text.splitlines()): + if line.lstrip().startswith("```"): + fence = not fence + yield index, line, True + continue + yield index, line, fence + + +def split(text: str, where: str) -> tuple[str, str, list[tuple[str, str]]]: + """Split a document into its title, the prose above the first section, and the sections.""" + title = "" + subtitle = True + preamble: list[str] = [] + sections: list[tuple[str, list[str]]] = [] + + for _, line, fenced in outside_fences(text): + if not fenced: + if not title and line.startswith("# "): + title = line[2:].strip() + continue + # A document may follow its heading with a tagline, bold and alone on its line, which + # completes the title rather than opening the prose. + if title and subtitle and line.strip(): + match = SUBTITLE.match(line.strip()) + subtitle = False + if match and not sections: + title = f"{title} - {match.group(1)}" + continue + if line.startswith("## "): + sections.append((line[3:].strip(), [])) + continue + (sections[-1][1] if sections else preamble).append(line) + + if not title: + fail(f"{where} has no level-one heading to title its chapter") + return title, "\n".join(preamble).strip(), [(t, "\n".join(b).strip()) for t, b in sections] + + +def target(link: str, page: dict[str, str] | None, chapter: str, repo: pathlib.Path) -> str: + """Point a document-relative link at the staged site, or at the file on GitHub.""" + if re.match(r"^[a-z][a-z0-9+.-]*:", link) or link.startswith("//"): + return link + if link.startswith("#"): + # The front page keeps its own anchors; a chapter has none, its sections being pages. + if page is None: + return link + anchor = link[1:] + if anchor not in page: + fail(f"link to #{anchor} matches no section") + return f"{chapter}/{page[anchor]}" + + path, _, anchor = link.partition("#") + for source, elsewhere, _split in DOCUMENTS: + if path == source: + return f"{elsewhere}/index" + if not (repo / path).exists(): + fail(f"link to {path} matches no file in the repository") + kind = "tree" if (repo / path).is_dir() else "blob" + return f"{REPO}/{kind}/main/{path.rstrip('/')}" + (f"#{anchor}" if anchor else "") + + +def outside_code(line: str, rewrite) -> str: + """Apply a rewrite to the prose of a line, leaving its inline code alone. + + A lambda in inline code (`[](auto)`) is otherwise indistinguishable from a markdown link. + """ + parts, last = [], 0 + for span in CODE_SPAN.finditer(line): + parts += [rewrite(line[last:span.start()]), span.group(0)] + last = span.end() + return "".join(parts) + rewrite(line[last:]) + + +def render(body: str, page: dict[str, str] | None, number: dict[str, tuple[str, str]], + chapter: str | None, repo: pathlib.Path) -> str: + """Rewrite a body for the site: heading depth, links, cross-references.""" + def prose(text: str) -> str: + text = INLINE_LINK.sub( + lambda m: f"({target(m.group(1), page, chapter, repo)}{m.group(2)})", text) + text = REFERENCE_LINK.sub( + lambda m: m.group(1) + target(m.group(2), page, chapter, repo), text) + return SECTION_REF.sub( + lambda m: f"[{number[m.group(1)][0]}]({chapter}/{number[m.group(1)][1]})" + if m.group(1) in number + else fail(f"reference to Section {m.group(1)}, which {chapter} does not have"), text) + + lines = [] + for _, line, fenced in outside_fences(body): + if not fenced: + if BADGE.match(line.strip()): + continue + if chapter is not None: + # The section's own heading became the page title, so its subheadings move up to + # take its place in znai's per-page navigation. + line = HEADING.sub(lambda m: m.group(1)[1:], line) + line = outside_code(line, prose) + lines.append(line) + return re.sub(r"\n{3,}", "\n\n", "\n".join(lines)) + + +def write(path: pathlib.Path, title: str, body: str) -> None: + path.write_text(f"---\ntitle: {json.dumps(title)}\n---\n\n{body}\n", encoding="utf-8") + + +def chapter(source: pathlib.Path, name: str, split_sections: bool, + out: pathlib.Path, repo: pathlib.Path) -> list[str]: + """Write one chapter and report its page names, in order, for the toc.""" + if not source.exists(): + fail(f"{source.name} does not exist") + title, preamble, sections = split(source.read_text(encoding="utf-8"), source.name) + if split_sections and not sections: + fail(f"{source.name} has no level-two headings to split into pages") + directory = out / name + directory.mkdir(parents=True, exist_ok=True) + + if not split_sections: + body = "\n\n".join([preamble] + [f"## {heading}\n\n{text}" for heading, text in sections]) + write(directory / "index.md", title, render(body, None, {}, None, repo)) + return ["index"] + + # Resolve every name a link may use before rendering any of it: both the anchor GitHub + # would have produced for the original heading, and the number the prose refers to. + page = {slug(heading): slug(NUMBERED.sub("", heading)) for heading, _ in sections} + number = {NUMBERED.match(h).group(0).rstrip(". "): (NUMBERED.sub("", h), slug(NUMBERED.sub("", h))) + for h, _ in sections if NUMBERED.match(h)} + + write(directory / "index.md", title, render(preamble, page, number, name, repo)) + names = ["index"] + for heading, body in sections: + stripped = NUMBERED.sub("", heading) + write(directory / f"{slug(stripped)}.md", stripped, render(body, page, number, name, repo)) + names.append(slug(stripped)) + return names + + +def toc(out: pathlib.Path, generated: dict[str, list[str]]) -> None: + """Rebuild the toc with the generated chapters ahead of the ones docs/toc names.""" + hand_written = (out / "toc").read_text(encoding="utf-8").strip().splitlines() + lines = [] + for section in SECTIONS: + if section is HAND_WRITTEN: + lines += hand_written + continue + source, name, _split = section + # A section is named for the document it carries, so a reader browsing the site can tell + # which file of the repository they are reading; the pages keep the document's own titles. + title = pathlib.Path(source).stem.replace("_", " ") + lines += [f"{name} {{title: {json.dumps(title)}}}"] + lines += [f" {page}" for page in generated[name]] + (out / "toc").write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("repo", type=pathlib.Path, help="repository root") + parser.add_argument("out", type=pathlib.Path, help="staged znai source directory") + args = parser.parse_args() + + repo, out = args.repo.resolve(), args.out.resolve() + if out.exists(): + shutil.rmtree(out) + shutil.copytree(repo / "docs", out) + + # docs/lookup-paths reaches the sources it quotes relatively, which the staged copy is no + # longer placed to do. + paths = (repo / "docs" / "lookup-paths").read_text(encoding="utf-8").split() + (out / "lookup-paths").write_text( + "".join(f"{(repo / 'docs' / path).resolve()}\n" for path in paths), encoding="utf-8") + + generated = {name: chapter(repo / source, name, split_sections, out, repo) + for source, name, split_sections in DOCUMENTS} + toc(out, generated) + + +if __name__ == "__main__": + main() diff --git a/scripts/sync_md_examples.py b/scripts/sync_md_examples.py new file mode 100755 index 00000000..58b9971a --- /dev/null +++ b/scripts/sync_md_examples.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Keep markdown code fences in sync with the compiled C++ examples they quote. + +A fence is quoted from a named region of an example: the example brackets the region with a pair of +`// sync-example-` comments, the document places `` immediately +above the ```cpp fence receiving it. A fence inside a blockquote keeps its `> ` prefix. Every +region must be quoted, so a broken anchor fails rather than letting the fence drift. +""" +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +# Markdown document -> the compiled example its fences quote. Keep the `files` pattern of the +# sync-md-examples hook in .pre-commit-config.yaml covering exactly these paths. +DOCUMENTS = { + "README.md": "examples/readme/main.cpp", + "TYPE_ALGEBRA.md": "examples/type_algebra/main.cpp", +} + +BOUNDARY = "// sync-example-" + +# Blockquote prefix, anchor comment, and the fence it introduces, body captured non-greedily. +FENCE = re.compile( + r"^([ >]*)[ \t]*\n[ >]*```cpp\n(.*?)\n[ >]*```[ \t]*$", + re.MULTILINE | re.DOTALL, +) + + +def fail(message: str) -> None: + sys.stderr.write(f"Error: {message}\n") + sys.exit(2) + + +def read(path: pathlib.Path, repo: pathlib.Path) -> str: + if not path.exists(): + fail(f"{path.relative_to(repo)} does not exist") + return path.read_text(encoding="utf-8") + + +def extract(example: pathlib.Path, repo: pathlib.Path) -> dict[str, list[str]]: + """Collect the regions an example offers for quotation, keyed by name.""" + where = example.relative_to(repo) + regions: dict[str, list[str]] = {} + open_name: str | None = None + body: list[str] = [] + + for lineno, line in enumerate(read(example, repo).splitlines(), start=1): + boundary = line.strip() + if not boundary.startswith(BOUNDARY): + if open_name is not None: + body.append(line) + continue + name = boundary[len(BOUNDARY) :] + if open_name is None: + if name in regions: + fail(f"{where}:{lineno}: region {name!r} opened again after it was closed") + open_name, body = name, [] + elif name == open_name: + regions[open_name] = body + open_name = None + else: + fail(f"{where}:{lineno}: region {open_name!r} closed by {name!r}") + + if open_name is not None: + fail(f"{where}: region {open_name!r} is never closed") + if not regions: + fail(f"{where} offers no regions to quote") + return regions + + +def sync(document: pathlib.Path, example: pathlib.Path, repo: pathlib.Path) -> bool: + """Rewrite the document's anchored fences from the example; report whether anything changed.""" + regions = extract(example, repo) + quoted: set[str] = set() + + def quote(match: re.Match) -> str: + prefix, name, _ = match.groups() + if name not in regions: + fail(f"{document.relative_to(repo)}: no region {name!r} in {example.relative_to(repo)}") + quoted.add(name) + body = "\n".join(f"{prefix}{line}".rstrip() for line in regions[name]) + return f"{prefix}\n{prefix}```cpp\n{body}\n{prefix}```" + + text = read(document, repo) + updated = FENCE.sub(quote, text) + + for name in sorted(set(regions) - quoted): + fail(f"region {name!r} of {example.relative_to(repo)} is never quoted by " + f"{document.relative_to(repo)}") + + if text == updated: + return False + document.write_text(updated, encoding="utf-8") + print(f"synced {document.relative_to(repo)} <- {example.relative_to(repo)}") + return True + + +def main() -> None: + parser = argparse.ArgumentParser(description="Synchronize markdown files with compiled C++ examples.") + parser.add_argument( + "documents", + nargs="*", + default=list(DOCUMENTS), + help=f"documents to synchronize, any of: {', '.join(DOCUMENTS)} (default: all)", + ) + args = parser.parse_args() + + repo = pathlib.Path(__file__).resolve().parents[1] + changed = False + for name in args.documents: + if name not in DOCUMENTS: + fail(f"unknown document {name!r}, expected one of: {', '.join(DOCUMENTS)}") + changed |= sync(repo / name, repo / DOCUMENTS[name], repo) + + if changed: + sys.exit(1) + + print(f"All code examples in {', '.join(args.documents)} are fully synchronized!") + + +if __name__ == "__main__": + main() diff --git a/scripts/sync_readme_example.py b/scripts/sync_readme_example.py deleted file mode 100644 index 1a4732a0..00000000 --- a/scripts/sync_readme_example.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -"""Keep the README.md code example in sync with examples/readme/main.cpp. - -examples/readme/main.cpp is the single source of truth: CI builds and runs it, -so the code README.md shows is proven to compile and pass its own checks. The -region between its two `// readme-example` marker lines is mirrored into the -```cpp fence under README.md's "## Example" heading. Run as a pre-commit hook: -it rewrites the fence and exits non-zero if it changed anything, so the commit -is blocked until the change is re-staged. -""" -import pathlib -import sys - -MARKER = "// readme-example" -HEADING = "## Example" - -repo = pathlib.Path(__file__).resolve().parents[1] -example_path = repo / "examples" / "readme" / "main.cpp" -readme_path = repo / "README.md" - -lines = example_path.read_text(encoding="utf-8").splitlines(keepends=True) -marks = [i for i, line in enumerate(lines) if line.rstrip("\r\n") == MARKER] -if len(marks) != 2: - sys.stderr.write( - f"{example_path.relative_to(repo)}: expected exactly two {MARKER!r} lines, found {len(marks)}\n" - ) - sys.exit(1) -region = lines[marks[0] + 1 : marks[1]] - -readme = readme_path.read_text(encoding="utf-8").splitlines(keepends=True) -try: - heading = next(i for i, line in enumerate(readme) if line.rstrip("\r\n") == HEADING) - fence = next(i for i in range(heading + 1, len(readme)) if readme[i].rstrip("\r\n") == "```cpp") - close = next(i for i in range(fence + 1, len(readme)) if readme[i].rstrip("\r\n") == "```") -except StopIteration: - sys.stderr.write( - f"{readme_path.relative_to(repo)}: could not find {HEADING!r} followed by a ```cpp fence\n" - ) - sys.exit(1) - -if readme[fence + 1 : close] != region: - readme[fence + 1 : close] = region - readme_path.write_text("".join(readme), encoding="utf-8") - print(f"synced {readme_path.relative_to(repo)} example <- {example_path.relative_to(repo)}") - sys.exit(1) -sys.exit(0)