Add TYPE_ALGEBRA - #359
Conversation
🤖 Augment PR SummarySummary: Adds a new design/spec document ( Changes:
Technical Notes: The docs CMake now requires a Python interpreter for staging and post-processing; docs generation becomes stricter by failing the build if the workaround scripts find nothing to patch. 🤖 Was this summary useful? React with 👍 or 👎 |
Add a comprehensive, mathematically rigorous technical specification of the type-level algebra and functional-composition model of libfn/functional. - Define core algebraic vocabulary (pack, copack, expected, optional, choice, and just) and map them to their corresponding ADT cardinalities (0, 1, A+B, AxB). - Detail graded error-set unioning (widening/subeffecting) and outline implicit-rejection safety boundaries alongside explicit narrowing via transform_error. - Document identity cluster transitions, explaining the decoupling design (member functions strict to carriers vs. namespace fn:: pipeline operators). - Explain type-tagged elimination (apply_type) using C++ standard state and constructor tags (std::in_place, fn::unexpect, std::nullopt, std::in_place_type). - Consolidate prose, remove redundant descriptions, and include 100% compile-verified C++20 code blocks demonstrating same-kind concept boundaries, pack::append splicing, and Cartesian product distribution. Assisted-by: Gemini:gemini-3.5-flash
e49ba49 to
d3ed77f
Compare
* Document simultaneous disjunction and conjunction's identity cluster support * Fix pack get projection ADL syntax and Section 9 cross-carrier constraints * Add singular lift and get extraction documentation to Section 3 * Elevate all mathematical notes to category-theoretically rigorous style * Eliminate duplicate graded monad definitions and fix apply_type carrier list Assisted-by: Gemini:gemini-3.6-flash
* Fold fn::as_pack directly into existing test_pack() example * Fold fn::as_copack and ADL get() into existing test_copack() example * Clean up and streamline prose for primary narrative * Ensure category-theoretic mathematical notes are consistent and rigorous Assisted-by: Gemini:gemini-3.6-flash
* Re-format copack vs copack_for under-the-hood section as standard [!NOTE] block * Re-format nested coproduct just/choice section as standard [!NOTE] block Assisted-by: Gemini:gemini-3.6-flash
* Detail how success mapping (transform) promotions work with pipeline operators * Detail how recovery (or_else/recover) and fallbacks (value_or) are compile-rejected or vacuously dead * Connect identity cluster behaviors systematically to the Section 12 operations map Assisted-by: Gemini:gemini-3.6-flash
* Move fail to the Success Channel group as a success-intercepting transition operation * Move recover to the Error/Empty Channel group as a failure-intercepting transition operation * Add explicit dual symmetry rules for fail and recover under Key Architectural Rules Assisted-by: Gemini:gemini-3.6-flash
…ary folds * Add operator| (disjunction / simultaneous sum composition) to the core operations list * Clarify that operator& represents conjunction * Explicitly introduce the fn::conjoin and fn::disjoin n-ary fold utilities Assisted-by: Gemini:gemini-3.6-flash
… TYPE_ALGEBRA.md * Correct copack apply description to note recursive unpacking of nested tuple-likes * Document the terminal data guarantee of normalized sum-of-products multidispatch * Refine carrier terminology to gently introduce 'monadic' context as functional jargon * Polished layout into two clean, readable paragraphs in Section 3 Assisted-by: Gemini:gemini-3.6-flash
* Fix spelling and grammatical structure of the constrained parameters warning * Fully integrate examples/type_algebra/main.cpp into the project CMake build * Enable pre-commit enforcement of the synchronized code regions * Verify compile-clean success across all 44 unit and example tests Assisted-by: Gemini:gemini-3.6-flash
…ions * Fix C++26 compilation errors with LIBFN_CXX26 * Polish copack, copack_for, choice, and choice_for terminology for strict ordering * Document and illustrate value-category preservation in deduction-only as_pack * Document and verify by-value decay and multi-argument coercion in explicit as_pack * Synchronize all code regions in TYPE_ALGEBRA.md with examples/type_algebra/main.cpp Assisted-by: Gemini:gemini-3.6-flash
…ions * Relax type algebra checks in examples to use stable choice instead of choice_for * Polish copack, copack_for, choice, and choice_for terminology for strict ordering * Document and illustrate value-category preservation in deduction-only as_pack * Document and verify by-value decay and multi-argument coercion in explicit as_pack * Synchronize all code regions in TYPE_ALGEBRA.md with examples/type_algebra/main.cpp * Document and verify the compile-time behavior of vacuous or_else recovery Assisted-by: Gemini:gemini-3.6-flash
* Reposition code block outside of the markdown blockquote for clean synchronization * Rewrite introductory transition sentence to flow naturally into test_vacuous_or_else() * Keep math notes and closure principle explanations fully intact * Upgrade sync_type_algebra_examples.py to support quoted code examples Assisted-by: Gemini:gemini-3.6-flash
* Group all four computation carriers in Section 3 and sum/product payloads in Section 4 * Unified and document Success-Path and Failure-Path bridging in Section 3 * Illustrate multi-alternative choice to optional bridging with heterogeneous success join * Fix stale Section references throughout TYPE_ALGEBRA.md * Synchronize all code fences with examples/type_algebra/main.cpp Assisted-by: Gemini:gemini-3.6-flash
71a2a91 to
b7c187c
Compare
Split carrier bridging by source: section 3 keeps fallible-to-fallible, section 10 takes every bridge from an identity carrier. Drop section 6's duplicate operator& example in favour of section 1's, and widen the shared example-types region to the types the later fences use. Each prose change carries a `<!-- why: ... -->` annotation recording the mechanism and its evidence, for the author's review. Assisted-by: Claude:claude-opus-5
Reformat examples/type_algebra at 100 columns (new .clang-format), turn example fixtures into parameters so the quoted fences show their types, and strengthen the assertions - including new witnesses for as_pack<T const &> forcing a reference element and for a reference-bearing pack stored in a copack. The disjoin example now shows the binary error-side product rather than unary forwarding. Prose: copack grades the value side as well as the error side; prefer copack_for/choice_for to naming copack/choice directly; member verbs cannot bridge carriers; sort-key collisions are a limitation of builds without std::type_order. Correct .value() on a fallible carrier - it is partial and throws, it does not discard the error channel. Assisted-by: Claude:claude-opus-5
Unify the separate sync scripts into scripts/sync_md_examples.py with individual pre-commit hook stanzas for README and TYPE_ALGEBRA. Apply prose shorthand rule to omit fn:: prefixes on core vocabulary types in README.md, and remove the temporal 'Now' from CONTRIBUTING.md. Use C++20 trailing return type styles in README.md and its compiled example. Assisted-by: Claude:gemini-1.5-pro
Assisted-by: Claude:claude-opus-5
Remove all 61 developer-review 'why' annotations. Systematically polish and refine the adjacent prose to adhere to standard WG21-level C++ terminology, including clarifications on compiler-specific type sorting, empty copack constraint-selected overloads, raw reference payload prohibitions on expected/choice/just, and singular monadic lift. Assisted-by: Claude:gemini-1.5-pro
…EBRA.md Highlight the symmetric dual nature of sum composition (disjunction) and product composition (conjunction) with reversed value/error behaviors. Clearly document that conjunction (operator&) works on both carriers and data whereas conjoin is data-only, and disjunction (operator| & disjoin) applies strictly to computation carriers. Assisted-by: Claude:gemini-1.5-pro
The operator constrains itself on the other operand, which is only safe where its own operand is deduced: as a hidden friend it could be spelled only as Policy::type<T, E>, a non-deduced context, so deduction rejected nothing and the constraint ran for every left operand there is. Where that operand reached this same operator by ADL, satisfaction depended on itself - a hard error, where the question should simply answer false. Declared at namespace scope instead, once per carrier; its three siblings constrain on the operands' channels rather than on the other operand, and keep their form. Closes #381 Assisted-by: Claude:claude-opus-5
znai renders a directory as a chapter and a file within it as a page, so each root document is split at its `##` boundaries into a chapter staged beside the API reference. Staging keeps the split in the build tree, leaving the documents whole in git and `docs/` hand-written throughout. Section numbering reaches the site as neither a title nor a dangling reference: a page carries the name alone, and the prose's `Section N` becomes a link to the page it names. Repo-relative links, which znai resolves as page references and rejects, become links to the sibling chapter or to the file on GitHub. The terminology table loses the backticks around its header cell because znai reads such a cell as plain text and dereferences null otherwise, in 1.73 and 1.91 alike. Assisted-by: Claude:claude-opus-5
docs/index.md restated README's introduction in a second, hand-maintained copy, and the two had already drifted: its How was a condensed form of README's, and its What a different framing of the same facts. README is now the front page itself, whole — splitting it would scatter an introduction written to be read in one go. The only claim the old page held alone, that the functor system is extensible, moves to README, where the section on what lies beyond the example already gathers capabilities of that kind. Assisted-by: Claude:claude-opus-5
Each root document becomes a section named after the file it came from, so a reader can tell what they are reading and where to find it: README, TYPE ALGEBRA, CONTRIBUTING, the hand-written chapters, CONTINUOUS INTEGRATION, and LICENSE last. Only TYPE_ALGEBRA is long enough to navigate better as pages; the rest stay whole. README titles its page by joining its heading to the tagline below it, so the site says "libfn - Functional programming in C++" without the script knowing it. The site keeps no index.md of its own — znai generates one redirecting the root to the first page. Badges are dropped on the way in. They belong on the repository page, and one of them stood as the whole of the License section, which now names the licence in words. Two repairs are made to the deployed site, neither having anywhere earlier to go. znai builds internal URLs as `/<doc-id>/<chapter>/<page>`, and this site is deployed at the root of its domain — so the doc id is empty and every link arrives as `//chapter/page`, which a browser resolves as a host. The navigation escapes this by never following its own hrefs; a link written in a page does not. `View on GitHub` was one link for the whole site, and a broken one: `/docs` without GitHub's `blob/main`. Each page now carries the path it was rendered from. Both repairs fail the build if they find nothing to do, so a znai release that fixes either will say so rather than leave the workaround standing. Assisted-by: Claude:claude-opus-5
The reference was organised by carrier, so a verb was documented once per carrier it applies to - byte-identical in seven cases of eleven, and differing only in which example it quoted in three more. A verb is one entity: it now has one page, carrying every carrier's examples, and the page names its category so the flat list reads as the grouping the chapters used to give. Only `transform_error` differed in substance: the operation is rejected on `optional`, which the optional page said and the expected page did not. The merged page says it. The concepts and the utilities reach the site for the first time, as does `fn::functor`, the extension point behind the pipeline. A concept renders as its description alone, doxygen giving znai no signature to show, so each carries a heading naming it - without which the page is forty unattributed paragraphs. Assisted-by: Claude:claude-opus-5
The section listed seven of the fifteen workflows, pointed at image definitions under paths that no longer exist, and named a registry the images left. It now groups the workflows by what they prove — that the library works, that it can be consumed, and that the containers the rest run in are current — and covers the packaging routes and the image tagging that were missing entirely. It also records why coverage and analysis each come in two workflows, which the YAML shows but does not explain: a pull request from a fork is given no credentials, so the build uploads an artifact and a second workflow reports from the base repository. Setting a fork up to report against your own accounts stays in CONTRIBUTING, which this links to rather than restates. Assisted-by: Claude:claude-opus-5
|
augment review |
znai renders a member's signature per overload but resolves the description once, by name, so every documentation stanza after the first on a name was dropped - pack::append's second stanza reached no page at all. Selecting an overload with args: gives each its own description, and the three plugins that need it all honour the option. The signature listing is written out in the page because znai cannot draw one: its doxygen node carries no ref-qualifier, so a member's four value categories render as four identical entries. doxygen_signatures.py holds every listing to the headers, and the docs build fails when they drift. conjoin and make each had overloads separated only by a requires-clause, which znai cannot select between; their stanzas are merged. Assisted-by: Claude:claude-opus-5
The carriers' own members were documented in the headers and rendered nowhere: expected::and_then, optional::or_else, copack::emplace and 45 others. Each now has a section on its carrier's page, so a reader looking up a carrier finds its whole surface. Where a specialisation declares a member the primary also declares, its listing joins that section rather than opening a second one of the same name. Three renderer corrections came with them. A constructor has no return type, so it is no longer drawn with one; a SFINAE-friendly trailing decltype over the library's internals names what a reader cannot, so it is left to the prose, as cppreference does with the same declarations; and a parameter type doxygen links is spelled by znai with a space after the link, which a selector must match. choice, copack and just each documented the implicit and explicit arms of a constructor pair separately, which znai cannot select between; their stanzas are merged. Assisted-by: Claude:claude-opus-5
expected<void, Err> and optional<T &> carried no documentation at all, while their primary templates were documented throughout - so a reader reaching either specialization found bare signatures. The words are not the primary's: over a void value side the callable is invoked with no arguments and apply_type's value arm receives std::in_place alone, and a reference optional has one overload of each member rather than a cv/ref set, hands the referent over as a plain T&, and has no value side to grade, so or_else must return that very type. disjoin_t::operator() was undocumented in both arities, its meaning carried only by the type's own stanza; it now mirrors conjoin's. Assisted-by: Claude:claude-opus-5
Member types, constructors, assignment, swap and the comparisons carried no documentation at all, so the reference showed each carrier's verbs without ever showing how one is made. Each now has a stanza - one line, naming the member rather than explaining it, because a copy constructor copies - and the pages carry them grouped above the verbs: what the type is and how it is built, before what it does. Most of these were not merely undocumented but invisible. doxygen parses a C++20 conditional-explicit constructor as a member named `explicit`, so every such constructor was absent from the XML entirely; expanding `explicit(x)` to `explicit` recovers all of them. One znai limit is now reported rather than worked around: it normalizes a query with `,\s+` -> `,` but leaves the member's own spelling alone, so a parameter type carrying a comma inside its template arguments - expected<U, G> const & - can be listed but never selected for. Four converting constructors lose their description to this; the checker names them, and says so as a note rather than failing a build no edit here could make pass. Assisted-by: Claude:claude-opus-5
The vocabulary landed in two passes and so landed in two places: just listed its constructors both under Construction and under a section of their own, and copack did the same for its constructors, has_type and operator=. Every listing of a kind now sits in one section, whichever of the page's compounds declares it. The sections run member types, construction, destruction, emplace, the remaining special functions, then the verbs - what the type is and how it is built before what it does. choice's deduction guides are named as such rather than sharing a heading with its constructors, and the prose that described the constructors moves with them. Assisted-by: Claude:claude-opus-5
A reader writes fn::transform, not fn::transform_t, and the object carried nothing while its type carried everything. Each of the thirteen now says what a pipeline does with it - the object is the trailing declarator of its own type's definition, so the description is doxygen's trailing form - and each verb's page lists it. Variables were being rendered as calls: `static auto size -> std::size_t` is not a declaration anyone could write. A variable is now spelled as its type and name, with the initializer that is the whole point of a constant like `size`, and without the elaborated `struct fn::` doxygen puts in front of a verb object's own type. Assisted-by: Claude:claude-opus-5
optional's twenty-three free operators and swap, and expected's comparison against a value, carried nothing. Each now says what it answers, and a page gathers them: the whole set for a carrier in one place, which is how a reader asks whether an empty operand compares at all. It carries no "Defined in" badge on purpose - these are declared beside the carrier they serve, across five headers, and one badge would be wrong for four of them. Placement went by matching each declaration's own text, not by doxygen's line numbers: doxygen reports the same source line for operator>= and operator<=>, which an earlier line-keyed attempt turned into malformed comments attached to the wrong overloads. fn::operator== against a value on an expected is listed without its description - `expected<T, Err> const &` carries a comma inside its template arguments, which znai's selector normalization cannot name. Assisted-by: Claude:claude-opus-5
The conjunction and disjunction are the algebra; conjoin and disjoin are only their n-ary folds, and those had pages while the operators had none. Each fold page now carries its operator's whole dispatch surface - nineteen arms for &, eight for | - above the fold that builds on it. The description had to move to reach the site. All the carrier arms share the parameter signature (Lh &&, Rh &&), and the reference renders one description per signature, taking the first arm doxygen reports: choice.hpp's, which carried none, so the expected and optional stanzas described overloads nobody could reach. One merged description now sits on that first arm and covers every carrier; the two it replaces stay in their own headers as ordinary comments. pack's data conjunction keeps its own stanza - taking (auto &&, auto &&), it is separately selectable. Assisted-by: Claude:claude-opus-5
pfn is a polyfill: sixty of its members were undocumented because they are std::expected's and std::optional's members, and a second copy of that specification would only be a second thing to keep true. Each type now says so and the page links where they are specified, so the silence reads as a decision rather than an omission. The links live in the page, not the stanzas. doxygen turns a bare URL into a <ulink>, which znai drops - the address survived as text a reader would have to copy out by hand - while an ordinary markdown link in the page renders as a link. Assisted-by: Claude:claude-opus-5
The concepts page claimed one header for all forty of its concepts. Twenty-eight are declared elsewhere - with the feature they constrain, across nineteen headers - so a reader including what the page said got neither some_monadic_type nor most of the rest. Each concept now names its own header, and the page says why they are not gathered. fn::choice had no description at all: the page asked for the compound `fn::choice`, which is the incomplete declaration and carries nothing, where the documented type is the constrained partial specialization. znai renders a compound that does not exist as silence, so the page had been arriving without its opening paragraph unnoticed. Assisted-by: Claude:claude-opus-5
znai renders a description it cannot find as silence, so documentation has gone missing here three separate ways and each time the build stayed green: forty-eight carrier members documented and named by no page, fn::choice asking for a compound that carries nothing, and operator& rendering the first overload's description when the documented overload was not first. check_docs_coverage.py fails on all three. Seventeen entities that were documented and unreachable are published with it - the free lifts and as_pack, fn::get, the applicability traits of both namespaces, and value_or's call operator, the one verb whose operator() no page carried. The ten that remain are the verbs' dispatch objects, listed in coverage-exemptions.txt with the reason; an entry that stops being needed is reported too, so the file cannot rot. Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-fable-5
Assisted-by: Claude:claude-fable-5
* Remove conversational filler, "AI slop" tells, and announcing phrases
* Simplify explanations and lists across all sections to make them active and direct
* Replace grand mechanism names (such as "the graded gateway") with clear, straightforward terms ("the entry point")
* Explicitly classify libfn's own pack and copack as raw passive data layouts alongside std::tuple and std::variant
* Improve opening paragraph wording to "as operations are chained into expressions"
* Rigorously preserve all factual C++ mechanics, mathematical notes, and the ordering rule
Assisted-by: Gemini:gemini-3.6-flash
An example region that no fence quotes now fails the sync instead of warning, and staging keeps a bare image line - only a linked image is a badge. Both flagged by review on #359. Assisted-by: Claude:claude-fable-5
|
augment review |
|
This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment "augment review" and we will review it. |
|
augment review |
…nsistency * Sync stale fn::operator& in conjoin.md and fn::operator| in disjoin.md with C++ headers * Add Doxygen specialization/inheritance artifacts for void and reference types to docs/coverage-exemptions.txt to satisfy docs_check_coverage Assisted-by: Gemini:gemini-3.6-flash
|



Add a comprehensive, mathematically rigorous technical specification of the type-level algebra and functional-composition model of libfn/functional.
Documentation site
TYPE_ALGEBRA now renders on libfn.org alongside README, CONTRIBUTING and LICENSE, each generated from the document itself rather than copied into a second place that can drift.
Sections. The site reads README · TYPE ALGEBRA · CONTRIBUTING · REFERENCE · CONTINUOUS INTEGRATION · LICENSE. A section is named for the file it carries and its pages keep the document's own headings. TYPE_ALGEBRA splits at its
##boundaries into eighteen pages, itsSection Ncross-references becoming links to the pages they name; the other documents stay whole. Section numbering never reaches the site — a page carries the title alone.docs/index.mdis gone: it restated README's introduction in a hand-maintained copy, and the two had already drifted, so README is the front page and znai generates the redirect to it. Badges are stripped on the way in; they belong on the repository page.Reference. Ten per-carrier chapters became one flat section with a page per feature, the category in the page title —
type fn::pack,monad fn::expected,functor fn::and_then,fold fn::conjoin,multidispatch fn::apply,other fn concepts,polyfills pfn. A verb had been documented once per carrier it applies to, byte-identical in seven cases of eleven. Twenty-four pages replace thirty-eight, verified lossless: no distinct doxygen member lost, every example region preserved. The concepts, the utilities, andfn::functor— the extension point behind the pipeline — reach the site for the first time.Continuous integration. The section named seven of the fifteen workflows and pointed at image paths that no longer exist. It now describes the pipeline as it is, including why coverage and analysis each come in two workflows: a pull request from a fork is given no credentials, so the build uploads an artifact and a second workflow reports from the base repository.
Generation
scripts/stage_docs_source.pystages the znai source into the build tree, so the documents stay single files in git anddocs/stays hand-written.scripts/fix_site_urls.pyrepairs the deployed output, which znai leaves in two states it cannot be talked out of://chapter/page, a network-path reference that sends the browser to a host named after the chapter. None of the four link spellings znai accepts avoids it.View on GitHubwas one link for the whole site, and a broken one (/docs, without GitHub'sblob/main). Each page now carries the path it was rendered from.Both repairs fail the build if they find nothing to do, so a znai release that fixes either will say so rather than leave the workaround standing quietly.
Doxygen stays at 1.12.0. 1.17.0 was tried and rejected: it exits zero while silently rendering the
requires(not some_copack<value_type>)overload ofcopack_valueonce where 1.12.0 renders it twice, on both the expected and optional pages.Verification
docsruns on every pull request, so a dangling link, an unresolvable directive or a missing page fails review rather than the deployment — znai is strict about all three, which is how each of the problems above surfaced. The generation scripts carry no test of their own: the docs build is their gate, and there is no python test harness in the repository to host one.Document principles
All important concepts appear early in a short introduction, and are expanded further in the following sections. Less important concepts are introduced in the sections where they are most useful. No concept is used before it is introduced. The document should be easy to any C++ software engineer to read and digest, and it should be also informative for these versed in category theory.
1. The Opening Map (Preamble & Chapter 3)
pack/copack) and Computation Carriers (optional/expected/just/choice) are first introduced as a sparse, high-level list in the main preamble.just,choiceandexpected<T, copack<>>form the identity cluster." It does not expand on its operations or mathematical properties here, keeping the introduction minimal.2. Core Algebraic Foundations (Chapters 2, 3, & 4)
copack<>) from Unit (pack<>), and definescopackset semantics (deduplication, flattening, sorting) and why the algebra is strictly opt-in.packandcopack(ADLget, structured bindings,.append(), singular copacks).3. Progressive Functional Composition (Chapters 5–9)
transform/transform_error).operator&): Multiplies values intopacks and unions errors intocopacks.operator|): Unions values intocopacks and multiplies errors intopacks.and_then): Introduces Kleisli arrows and monadic sequencing of success paths.4. Advanced Algebraic Crossings (Chapters 10 & 11)
choiceMonad): Explains the specific newtype monadic properties ofchoice, how it behaves atomically compared to flat copack data, and its categorical laws.5. Elimination & Reference (Chapters 12–15)
applyandapply_type.Assisted-by: Claude:claude-opus-5
Assisted-by: Gemini:gemini-3.6-flash