feat(core): Unfold — the build-only/many optic citizen - #26
Merged
Merged
Conversation
Inhabits the last empty cell of the focus-shape × capability lattice: Unfold[T, B, F] = Optic[Unit, T, Unit, B, Forget[F]] whose real map is embed: F[B] => T — the algebra of a recursion scheme and the "assemble one whole from many parts" arrow, as Review is to Getter on the many rung (spike: docs/brainstorms/2026-06-10-unfold-build-many-citizen.md). - core: final-class Unfold with two factories — apply (Applicative carriers; honest vestigial to = pure(())) and algebra (pattern functors, which admit no Applicative; vestigial to fails loudly). Fused andThen members: Unfold∘Review (Functor), Unfold∘Unfold (Applicative, algebraic-lens pull), and the generalized Unfold ∘ any-reversible-inner (ReverseAccessor[G]). - core: Review.andThen(Unfold) fused member + the reversible-outer ∘ Unfold extension — the many-rung mirrors of the Review collapses. - core: direct2forget's documented-unreachable `???` from is now a sound branch (Foldable[F] singleton-pick; the only reachable F[B] is monadicPull's pure(b)). Unfold made the branch reachable. - laws: UnfoldLaws + UnfoldTests — constructor-correctness, pre/post-compose coherence, vestigial singleton degradation (Applicative-only RuleSet); registered in OpticsLawsSpec for a List carrier and a pattern functor. - tests: composition matrix extended to the 11-family grid (121 cells); the Unfold row/column mirrors Review's reversibility pattern. UnfoldSpec pins behaviour incl. the formerly-??? path. - schemes: cata overload consuming a pure PSVec algebra carried as an Unfold (node-blind by design; the typed path is where pure algebras are fully expressive — Embed[F, S] ≅ Unfold[S, S, F], future work). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
🚀 Cloudflare Pages preview for https://8ea6a4ec.cats-eo-docs.pages.dev Branch alias: https://feat-unfold-build-many-citiz.cats-eo-docs.pages.dev Built from commit |
Site: - optics.md: family taxonomy redrawn with all three one-way rungs (read-only Getter→AffineFold→Fold, build-only Review→Unfold, write-only Setter) and the collapse rules; NEW full 11-family / 121-cell composition matrix table (pinned by CompositionMatrixSpec) with the void-structure rationale; NEW Unfold reference section (both factories, pattern-functor composition, mdoc-run examples); ReadOnly[F] → ReadCompose, DirectGetter → Getter, AffineFold alias corrected to B = Unit, "lens ∘ affineFold doesn't type-check" corrected (it composes via ReadCompose), removed the cross-F `~>` Fold extension section (the extension no longer exists — the read-collapse covers it), pointers re-aimed at the spec. - concepts.md: Direct → Forget[F] edge added to the Composer lattice (direct2forget, now with a sound build side); carrier table gains Review (Direct) and Unfold (Forget[F]). - schemes.md: "the algebra as an optic" — cata(Unfold) overload with the node-blind honesty note. - migration-from-monocle.md: "Getter/Setter don't compose" replaced with the collapse story; Review/Unfold no-Monocle-equivalent row. - multifocus.md / index.md / benchmarks.md: matrix pointers freshened, DirectGetter → Getter. Scaladoc: - Lens: broken [[DirectGetter.andThen]] link → [[Getter.andThen]]. - Optic companion: extension catalogue mentions the Unfold collapse. - Review / Fold / Forget: cross-links to the Unfold dual. - GetterLaws: direct2forget parenthetical updated (sound pick, not "reachably-unsound-free"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The write-only family's old name overlapped with the build group's vocabulary: a "Setter" sounds like it sets/constructs, but the family is the modify capability — `(A => B) => S => T` — with no build side at all (Review / Unfold own building). Renaming makes the taxonomy's three one-way rungs unambiguous: read-only (Getter/AffineFold/Fold), build-only (Review/Unfold), write-only (Modify). - core: `optics.Setter` → `optics.Modify` (final class + companion), `data.SetterF` → `data.ModifyF` (carrier; stored pair accessor `setter` → `modifier`); given names follow (`assocModifyF`, `tuple2modify`, `either2modify`, `affine2modify`, `multifocus2modify`, `coerceToModify`). - laws: `SetterLaws`/`SetterTests` → `ModifyLaws`/`ModifyTests` (RuleSet method `setter` → `modify`); `SetterFLaws`/`SetterFTests` → `ModifyFLaws`/`ModifyFTests`. The Monocle provenance note keeps pointing at `monocle.law.SetterLaws` (their name). - tests: composition-matrix row/cells renamed (`modify ∘ …`), fixtures `o_setter`/`i_setter` → `o_modify`/`i_modify`; behaviour spec titles updated. - benchmarks: eo-side alias `EoSetter` → `EoModify` and carrier refs updated; `SetterBench` CLASS NAME and method names kept verbatim so CI benchmark tracking history stays continuous (Monocle's family is still `Setter` — `MSetter`/`mSet` untouched). - site + README: family list, taxonomy diagram, composition matrix, carrier tables, Composer lattice, migration guide (Monocle `Setter` → eo `Modify` row; left column keeps Monocle's name), anchors `#setter` → `#modify`. Also fixed two stale claims found in the sweep: concepts.md still said Review sits outside the Optic trait (false since the Review→Optic fold-in), and benchmarks.md still named `SetterOptic` (a class renamed before this change). README family list also gains the missing Unfold entry. No deprecation shims: no published baseline exists (mima.sbt — 0.1.0 has no previous version), so the rename is a pre-release cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flat taxonomy flowchart conflated the family space's three independent axes. Replace the lead diagram with a hand-authored isometric SVG (static/optic-taxonomy-3d.svg) over: x — focus arity one / 0-or-1 / many y — from side contextual write / total build / none z — read side present (top plane) / absent (bottom plane) The geometry surfaces facts the flat graph could not: Lens/Iso and Optional/Prism differ only on the write-vs-build axis (the Modify rename rationale, now visible); the read-only and build-only rails run parallel (Fold ↔ Unfold mirror); the (0-or-1, build) cell collapses into Review (mend is total); the (many, read, total-build) cell is uninhabited; Modify spans the arity axis as the write-only bottom. - Rendered as a pure SVG <img> — no JS, dark mode via an internal prefers-color-scheme stylesheet matching the Helium palette; verified by headless-browser screenshots in both schemes. - Generator script committed at site/tools/gen-taxonomy-svg.py (deterministic geometry; rerun to regenerate after palette or family changes). - The mermaid graph stays, retitled "Composition joins" — edges are about composition joins, which read better in 2D; the matrix table remains the cell-level truth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nality; drop the mermaid graph
Redesign the isometric taxonomy around the composition machinery
itself and make it the sole family diagram (the mermaid joins graph
is gone — the matrix table is the cell-level truth, the figure the
geometric intuition):
x — read cardinality 1 / 0-or-1 / N (the ReadCompose join axis)
y — write cardinality 1 / 0-or-1 / N (a future WriteCompose)
z — capability read-only (top), read-write (middle),
write/build-only (bottom)
Read-only families collapse to a rail on the read axis (Getter /
AffineFold / Fold — the top layer); write/build-only families to a
rail on the write axis (Review / Unfold + the cardinality-agnostic
Modify bar — the bottom layer); the read-write grid pairs the axes
(Iso·Lens at (1,1), Prism·Optional at (0-or-1,1), Traversal at
(N,N)). Read-collapse = projection up onto the top layer at the
ReadCompose join; write/build-collapse = projection down.
The write-cardinality 0-or-1 cells (fallible write / fallible build)
are rendered as planned (tan, dashed) and pointed at the
failure-typed-build spike; a scope addendum in the BiAffine
brainstorm pulls WriteCompose and the missing-cell carriers into
that plan explicitly.
Verified by headless-browser screenshots (light + dark) of both the
standalone SVG and the built optics.html.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the cardinality axes with the sharper pair (user-designed):
x — focus nature total / fallible / multiple (how `to` lands;
exactly the ReadCompose join lattice)
y — source nature total / contextual / fallible (what `from`
needs: bijection-or-mend / leftover X / can
reject — the WriteCompose / BiAffine rung)
z — capability read-only / read-write / write-only (unchanged)
The payoff over the cardinality axes: the read-write grid becomes
COMPLETELY inhabited — Iso (total,total), Lens (total,contextual),
Prism (fallible,total), Optional (fallible,contextual), Traversal
spanning (multiple × {total,contextual}) since fixed-shape/Grate
rebuilds totally via tabulate while `each` rebuilds contextually —
and the only remaining row is the planned fallible-source one
(fallible write / BiAffine / fallible each), which maps one-to-one
onto the failure-typed-build spike's candidates. Iso/Lens and
Prism/Optional get their own cells back, and both one-way rails now
run parallel along the focus axis (Review consumes a total focus,
Unfold a multiple one).
BiAffine brainstorm addendum updated to the new axes: with this
basis, every remaining hole in the family space IS that spike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A write-only optic's `from` is real (only `to` is vestigial), so both axes apply on the bottom layer, and each family sits directly below the read-write cells whose build half it is: Review below Iso (with ≡ Review below Prism — mend is total), Unfold below Traversal's total flank, Modify as the entire contextual row (it IS the contextual write half of Lens / Optional / each), fallible build as the planned fallible row. The vertical drop now MEANS something: write-only = the layer above minus its read side. Only the read-only top layer remains a rail — its `from` is genuinely absent. Also fixes the previous layout's real error: `fallible build` was railed between Review and Unfold as if it differed in focus nature; it differs in source nature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extend Getter / AffineFold / Fold into bars across the source axis: read-only optics pin B = Unit (the terminal type, NOT Nothing), so a function into Unit exists uniformly for every input and the vestigial `from` is vacuously satisfied at every source nature — the read-only families honestly hold the whole axis, the exact dual of Modify spanning the focus axis on the write-only layer. (Nothing in that slot would admit no `from` at all.) With this, all three capability layers are full planes over the same focus × source footprint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review is the build half of Iso AND Prism (a mend is total), so it is one tile spanning both cells on the write-only plane — not a tile plus a ≡-ghost leaving a visual hole. Bottom plane is now gaplessly tiled: Review (span) + Unfold on the total row, Modify on the contextual row, fallible build (planned) on the fallible row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the three integration pages into site/docs/integrations/ with their own directory.conf (title + order: circe, avro, jsoniter); the root navigationOrder lists the directory in their old slot. All relative links rewritten (root pages → integrations/<page>.md, moved pages → ../<page>.md, intra-integration links unchanged) and the README's absolute eo.constructive.dev/<page>.html URLs updated to /integrations/. Page URLs change accordingly (old root-level circe/avro/jsoniter.html paths are gone). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ency Fact-checked against monocle-core/monocle-law 3.2 (via cellar) and Maven Central, and corrected accordingly: - REMOVED "Polymorphic constructors" as a divergence — Monocle ships the full PLens/PPrism/POptional/PTraversal/PSetter hierarchy; the cheat sheet now maps PLens.apply to Lens.pLens instead. - REMOVED "Discipline law instances" as a divergence — Monocle ships monocle-law with the same discipline rule-set pattern; reframed as "law testing ports directly" (an import swap), which is the truth. - CORRECTED the circe section: the module is circe-optics (not "monocle-circe"), it is published for Scala 3, and JsonPath does navigate the AST without a full decode. eo's actual differentiators stated instead: multi-field foci, the observable Ior failure channel, and the extension to Avro records and raw jsoniter bytes. - Tone: Monocle is never "incapable" or "missing" — phrasings are now "prefers", "chooses", "expresses through", "leaves to dedicated libraries", with its design goals credited. Structure per review: a junior-friendly "Why migrate?" opener stating eo's actual positioning — performance and industrial application; the internals are not the most elegant code, what you buy is efficiency, stack safety, interoperability (wire formats), and compositional reach. Capability content refreshed to current eo (Unfold, Modify, recursion schemes, the 11-family compiler-pinned matrix, BijectionIso full-cover upgrade). Cheat sheet moved to the bottom and extended (pLens row, laws row, schemes row). Benchmark claims restated within what the published tables support (~2x at depth 6, ~3x traversals, Monocle wins some single-hop cells). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both subsections claimed differences Monocle doesn't actually have: - ".andThen auto-morphs" implied cross-family composition is an eo feature; Monocle's hierarchy composes across families just as well (and the page already said so). Rewritten as "Bring your own optic": the real difference is that eo has NO extension hierarchy — composition is typeclass-driven over the carrier, so an optic from outside the shipped families needs no blessed place in a subtype tree: implement the trait, provide the instances, compose with everything. Links to Extensibility. - "Read-only and write-only chains collapse" implied read-only collapse is an eo feature; Monocle's hierarchy lands on Getter/Fold just as naturally. Rewritten as "The one-way side, built out in both directions": what eo actually adds is the fully populated build/write-only direction (Modify, Review, Unfold; fallible tier planned) and the preliminary recursion-schemes integration — schemes as optics so recursive-structure algorithms sit in the same pipeline as getters and modifiers, heading toward whole request-to-Kafka flows as one composed optic. Leads into the family-roster section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kryptt
added a commit
that referenced
this pull request
Sep 4, 2026
…ed upcast, formatting The concurrent force-push rebased the branch onto a newer main (#26, Unfold): Miss dropped its second type parameter (Miss[A] extends Affine[A, Nothing], re-typing subsumed by subtyping, widenB gone). Adapts the C8/BiAffine-drop commit's touched files to that arity — Graft[Affine] constructors, Apo.scatter, the spec toys (widenB check rewritten as the allocation-free upcast it now is) — plus scalafmt on the drifted files.
kryptt
added a commit
that referenced
this pull request
Oct 1, 2026
…ed upcast, formatting The concurrent force-push rebased the branch onto a newer main (#26, Unfold): Miss dropped its second type parameter (Miss[A] extends Affine[A, Nothing], re-typing subsumed by subtyping, widenB gone). Adapts the C8/BiAffine-drop commit's touched files to that arity — Graft[Affine] constructors, Apo.scatter, the spec toys (widenB check rewritten as the allocation-free upcast it now is) — plus scalafmt on the drifted files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Inhabits the last empty cell of the optic lattice (focus shape × capability): the build-only / many citizen.
Unfold[T, B, F] = Optic[Unit, T, Unit, B, Forget[F]]whose real map isUnfoldis toReviewexactly whatFoldis toGetter— the many-rung build-only mirror. It is the F-shapeembedof aCorecursiveinstance (Fold'sS => F[A]beingproject), and the "assemble one whole from many parts" aggregation arrow that previously had no optic.Spike + findings:
docs/brainstorms/2026-06-10-unfold-build-many-citizen.md(Q1–Q6 all resolved in-doc).What's new
core/optics/Unfold.scala— final class storingembed(per the encoding findings), with two factories:Unfold.apply(F: Applicative) — honest vestigialto = pure(()); read-side ops degrade to the singleton layer (law-pinned).Unfold.algebra(constraint-free) — for pattern functors (BinF,RoseF, …), which admitTraversebut noApplicative(purecannot pick a constructor). Their vestigialtofails loudly if forced.Review's reversibility pattern in both directions:unfold.andThen(review)— pre-process each part (Functor[F]only, pattern functors included)unfold.andThen(unfold)— layered algebras (Applicative[F], theassocForgetMonadpull)unfold.andThen(iso | prism)— generalized reversible-inner overload (ReverseAccessor[G])review.andThen(unfold)fused member + the reversible-outer ∘ Unfold extension (iso/prism.andThen(unfold))direct2forget's???is now a sound branch — core's last hole.Unfoldmade the liftedfromreachable (aMonad[F]Forgetchain executes it); theFoldable[F]singleton-pick is total on every reachable path because the onlyF[B]ever fed there ismonadicPull'spure(b). Pinned by a behaviour test.laws:UnfoldLaws+UnfoldTestsdiscipline rulesets — constructor-correctness (theGetterLawspattern), pre/post-compose coherence (a composed algebra cannot drift from its parts), and the vestigial singleton-degradation law (Applicative-only ruleset).Unfoldrow/column exactly mirrorsReview's:iso/prism/review/unfoldcompose (import-free, unascribed), everything else is void by design. Notablygetter ∘ unfoldis void because read-only optics haveB = Unit— the feat: recursion schemes as composable optics (+ honest Fold, structure-preserving cross) #23 honesty decision pays off here.schemes:cataoverload consuming a purePSVecalgebra carried as anUnfold— algebras can now be assembled by optic composition before the fold engine consumes them.Key decisions
Applicative[F]is NOT required (corrects the spike sketch): it would exclude pattern functors — the prime consumers. Two factories instead, each honest about its read surface.cata = plateFold.cross(unfold)was REFUTED (verified: type error —crossneeds reversibilityMultiFocus[PSVec]rightly lacks;catais a fixpoint, not a 2-optic composition). What shipped is the true version: the algebra as a composable citizen the engine consumes.PSVecalgebras are node-blind — the para-flavored(S, PSVec[A]) => Aoverload stays primary. The typed path is where pure algebras are fully expressive: feat(schemes): typed recursion schemes as composable optics — the zoo (para/apo/histo/futu, zygo/mutu, fused refolds, M drivers), Graft[Affine] build seam, fused cross #24'sEmbed[F, S]≅Unfold[S, S, F]— unifying them is the natural follow-up once feat(schemes): typed recursion schemes as composable optics — the zoo (para/apo/histo/futu, zygo/mutu, fused refolds, M drivers), Graft[Affine] build seam, fused cross #24 lands.Forget[F]carrier suffices (no structural leftover needed;X = Nothing);MultiFocusdeferred until a consumer needs the skeleton to survive alongside the build.Testing
UnfoldSpec(10 examples): embed, all five composition seams, the formerly-???morph-routed path, singleton degradation, loud failure for pattern-functor read ops.OpticsLawsSpec: Unfold rulesets for aListcarrier (full, incl. vestigial law) and aBinFpattern functor (Functor-only).CompositionMatrixSpec: 121/121 green — all 7 new inhabited cells landUnfoldunascribed; all 14 new void cells fail to compile as asserted.SchemesSpec: pure-algebracataequivalence + an optic-composed algebra driven through the engine.sbt test(pre-push).Rename:
Setter/SetterF→Modify/ModifyFWith the build group now fully populated (Review / Unfold), the write-only family's old name was ambiguous — a "Setter" sounds like it builds. Renamed to
Modify(family) /ModifyF(carrier), making the three one-way rungs unambiguous: read-only (Getter/AffineFold/Fold), build-only (Review/Unfold), write-only (Modify). Laws, tests, matrix, site, and README follow; benchmark class/method names are kept verbatim for CI tracking continuity (Monocle's family is stillSetter); no deprecation shims since no published baseline exists.Docs
optics.md: family taxonomy redrawn with all three one-way rungs (read-only Getter→AffineFold→Fold, build-only Review→Unfold, write-only Setter) plus the collapse rules; a full 11-family / 121-cell composition matrix table (mirroringCompositionMatrixSpec) with the void-structure rationale; a new Unfold reference section with mdoc-run examples (both factories, pattern-functor composition).ReadOnly[F]→ReadCompose,DirectGetter→Getter(incl. a broken scaladoc link inLens),AffineFoldalias corrected toB = Unit, "lens ∘ affineFolddoesn't type-check" corrected (it composes), removed the cross-F~>Fold extension section (the extension no longer exists), migration guide's "Getter/Setter don't compose" replaced with the collapse story.concepts.md:Direct → Forget[F]edge added to the Composer lattice; carrier table gains Review and Unfold.schemes.md: the algebra-as-an-optic section (cata(Unfold), node-blind honesty note). Review/Unfold ↔ Fold/Getter mirror cross-links added across core scaladocs.Deferred follow-ups
Embed[F, S] ≅ Unfold[S, S, F]unification in the typed schemes path (blocked on feat(schemes): typed recursion schemes as composable optics — the zoo (para/apo/histo/futu, zygo/mutu, fused refolds, M drivers), Graft[Affine] build seam, fused cross #24).MultiFocus-carried variant if a consumer ever needs leftover-preserving builds.Plan/spike doc:
docs/brainstorms/2026-06-10-unfold-build-many-citizen.md🤖 Generated with Claude Code