Repository navigation
feat(core): compose Affine → Getter (Optional/AffineFold andThen Getter = AffineFold) - #25
Merged
Merged
Conversation
…ter = AffineFold) Composing a partial optic with a read-only Getter now works and yields an AffineFold (a partial read): getOption the focus, then read it through the getter. A read-write/partial outer's back-focus B = A can't thread a Getter's Unit back-focus as a writable optic, so the read-only downgrade is the sound (and only) result — the mirror of how a Getter chain composes Getter-to-Getter. - Optional (concrete, read-write) andThen Getter: a new fused member on the Optional class (alongside its Optional/Lens/Prism/Iso fused andThens), returning AffineFold[S, C]. A member (not an extension) so it doesn't perturb the existing cross-carrier andThen resolution (e.g. Affine andThen MultiFocus). - AffineFold (read-only) andThen Getter: already composes via the morphed path (Morph[Affine, Direct] -> AffineFold); covered by the new test. Tests in OpticsBehaviorSpec (both outers -> AffineFold, hit/miss); docs note in optics.md. Full `sbt test` green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
🚀 Cloudflare Pages preview for https://72c4d62c.cats-eo-docs.pages.dev Branch alias: https://feat-affine-getter-compose.cats-eo-docs.pages.dev Built from commit |
…ed to/from
Two coupled changes, validated by three CI A/B benchmark pairs:
1. Read-only / write-only cross-carrier andThen, routed through the
Accessor/Morph machinery (no per-class fused overloads):
- andThenReadOnly: total readers (Accessor[F] — Iso/Getter/Lens)
collapse to DirectGetter — lens.andThen(getter) = Getter.
- andThenAffineFold: partial readers (NotGiven[Accessor[F]] —
Optional/AffineFold/Prism) compose their new `readOnly` projection
through Morph-to-Affine (rightToLeft for Affine, bothViaAffine for
Either) — optional/prism.andThen(getter) = AffineFold.
- andThenSetter: writable carriers morph through Composer[·, SetterF]
and re-wrap concrete — lens/optional.andThen(setter) = Setter.
- readOnly: the sound write-forgetting projection (T = B = Unit)
whose Unit seam makes the partial collapse line up.
2. Optic.to/from encoded as methods (def) instead of stored Function1
vals, across every optic, carrier, and integration:
- Construction: −36–41% B/op on composed-lens build (the two eager
closures per optic are gone).
- Steady-state: broad small CI wins (AffineFold/Optional/Setter/
Traversal/Avro/Plated, −5–25%).
- DirectGetter/Review stay FINAL classes with the function as a val
param + fused per-site andThen: the abstract-class variant centred
all leaf dispatch in one shared wrapper body, going megamorphic
(eoGet_3 +219%, eoAna +17% on CI; PrintInlining showed the
Function1.apply sites left as virtual calls). Final-class restores
the old-fused baseline (CI-confirmed, eoGet_3/6 −4%).
- Big-bodied integration optics (JsoniterPrism) keep def to/from as
1-call forwarders to private methods so the entry stays a few
bytes and inlines at every use site instead of blowing the
caller's C2 budget ("callee is too large" with direct bodies).
ReviewBench: new build-direction mirror of GetterBench (pre-built
depth-0/3/6 reverseGet vs hand-rolled floor) — Review composition was
previously unbenchmarked; composed Reviews now allocate identically to
hand-written builders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- AffineFold.apply/select now return a concrete `final class PickFold` (stores `pick`, fused getOption + andThen(PickFold/DirectGetter)) — removes the shared-anonymous-wrapper shape whose single `pick.apply` bytecode site goes megamorphic across instances (the PrintInlining finding from the abstract-class DirectGetter regression). - Optic.readOnly is `inline` (per-call-site splice, same rationale); E197 suppressed as the intended behaviour. - Optic.id returns BijectionIso so identity compositions take the fused members instead of the generic carrier path. - Drop the unused `[X]` type params on MendTearPrism/PickMendPrism modify/replace (they shadowed the class's `type X`). - Composer.direct2either: replace `???` on the uninhabited Left branch with Nothing-elimination (`case Left(x) => x`) — total and compiler-verified. - Refresh the Optic companion's operations catalogue (readOnly, cross, the andThen collapses). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped build - Unfold: inhabit the empty build-only/many lattice cell. Flags that the proposed `T => F[B]` is Fold-shaped (project/coalgebra) and the genuine gap is the dual `embed: F[B] => T` (algebra); ties it to direct2forget's `???`, Plated.plate's latent project/embed, and Schemes.cata/ana. - Failure-typed build: generalize Affine to miss on the build side too. Lays out 3 candidate encodings (focus-channel / BiAffine / Ior-carrier), the Optic.from signature fork, and the fallibility-as-third-axis question. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ue carriers, matrix spec Every semantically meaningful optic∘optic pair (80 of 100) now composes via plain .andThen with no expected-type ascription and no data.*.given imports; the other 20 are void by design (build through non-invertible, write through read-only, read through write-only) and are pinned as must-not-compile. - ReadCompose[FO, FI]: read-side composition across the read/write seam. Read-arity lattice (total/partial/many -> Getter/PickFold/ForgetFold[List]), keyed on Accessor/PartialAccessor with a low-priority ForgetfulFold fallback; instances in its own companion so they are in implicit scope for every carrier pair. Surfaced as members (extension fallback does not fire after member overload failure): a trait-level any∘read-only overload (inner T pinned to Unit, B left free so the same-carrier member stays strictly more specific), andThenReadAny on Getter/PickFold/ForgetFold (override-rehomed trait overload to break the signature-vs-owner overload tie), fused andThen(Getter) on BijectionIso/GetReplaceLens, and a new ForgetfulFold[Direct]. - Opaque carriers: ForgetK[F, X, A] / MultiFocusK[F, X, A] (opaque types cannot be curried, so uncurried opaque + thin curried alias keeps the Forget[F]/MultiFocus[F] carrier spelling). Dealiasing now stops at an implicit-scope anchor, so the carrier givens resolve with no import; object Forget/MultiFocus re-export the K companions for API and legacy import compatibility. Instance member signatures respelled in alias form (given-with instances are typed by their anonymous class, and dealiased signatures leak through inline extensions). PSVec cats instances moved to the PSVec companion — the actual implicit-scope anchor for Functor[PSVec]. - CompositionMatrixSpec: all 100 cells asserted via compiletime.testing (compiles unascribed + lands at the expected strength, or does not compile), deliberately import-free. Verified against a real-compiler all-pairs harness: 100/100 agreement. Carried from the previous session: Accessors.scala split into Accessor/PartialAccessor/ReverseAccessor, removal of the dangling ReadCompose references from the reverted spike, and the schemes.md DirectGetter -> Getter rename catch-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n-imports
Follow-up cleanup to the opaque-carrier conversion.
- Construct via the public carrier names: MultiFocus(x, fa) (façade apply) and
Forget(fa) (exported ForgetK.apply); removed the now-dead MultiFocusK.wrap.
No K-name escapes the two carrier definition files.
- Removed 33 redundant `import data.{MultiFocus,Forget,SetterF,Direct,Affine}.given`
across core/tests/laws/jsoniter/benchmarks. With every carrier's instances on
its own companion (opaque types are now implicit-scope anchors; SetterF/Direct/
Affine always had companions), `Foo[Carrier]` resolves with no import.
- Killed two byte-identical duplicate instances that the explicit imports had
been masking (imported givens outrank implicit-scope ones): the Affine
ForgetfulTraverse and ForgetfulFold now live ONLY on object Affine
(Affine.traverse / Affine.fold), matching the carrier-owns convention used by
Direct/SetterF/MultiFocus/Forget. object ForgetfulTraverse / ForgetfulFold now
hold only the stdlib carriers (Tuple2, Either) whose companions can't be
extended. This removes the latent ambiguity that surfaced once the imports were
gone, so no call site needs to disambiguate anymore.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Its only consumer, NamedTupleCodecSpike.scala, was deleted in 100b706; the macro object referenced it solely in its own docstring. Dead spike code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ce-review summaries and similar agent-tooling output land under .context/; they are per-run scratch, not project sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blank by default, so push / default dispatches are unchanged. Lets a manual run request `-prof gc` to capture allocation B/op — the metric that survives a noisy shared runner — without the filter-injection trick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adCompose The ReadCompose typeclass + the trait-level any∘read-only andThen overload form a correct generic for the whole read-collapse triangle, demoting the bespoke read-collapse members from load-bearing-for-compilation to perf fast-paths over a correct generic. Removed the redundant ones: - Tier A (generic, strictly dominated by the trait member): the two Optic.* extension `andThen(Getter)` collapses (Accessor -> Getter, PartialAccessor -> PickFold). A member always beats an extension, so these were already shadowed; they merged "two competing generic read-collapse paths" into the single ReadCompose path. - Tier B (single-seam terminal bespoke read-collapses): Prism∘Getter, PickMendPrism∘Getter, Optional∘Getter. - Tier C (the hot-path fast-paths added with ReadCompose): Iso∘Getter, Lens∘Getter, Getter∘PickFold, AffineFold∘Getter. All correctness-neutral: CompositionMatrixSpec stays 100/100 (ReadCompose returns the identical concrete type for every removed cell), full suite green, benchmarks compile. The KEPT fusions are the ones the generic cannot replace: same-family same-carrier (dodge the AssociativeFunctor round-trip + C2's recursive-inline cap on deep chains) and cross-family structure-preserving (writable concrete results; Morph would erase the type). The read-collapse result is single-seam terminal (B=Unit, can't extend the write side), so the removed members were never a deep-chain spine — the only cost is the fixed, depth-independent ReadCompose dispatch tax, to be characterised on CI bench. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…forgetful, compose packages
The 11 typeclasses that lived directly in `dev.constructive.eo` (and the
accessor trio that was awkwardly in `eo.data`) now sit in three packages by
responsibility:
- eo.accessor — Accessor, PartialAccessor, ReverseAccessor
(focus extract / inject primitives)
- eo.forgetful — ForgetfulFunctor, ForgetfulApplicative, ForgetfulFold,
ForgetfulTraverse (cats-lifted focus operations)
- eo.compose — AssociativeFunctor, Composer, Morph, ReadCompose
(optic-combination machinery; all import Optic)
The accessor/forgetful boundary is the read-primitive vs cats-operation split;
the compose boundary is objective — those four are exactly the typeclasses that
depend on Optic. Choice B of the three layouts considered.
These were visible to optics/ and data/ for free via enclosing-package scope
and to downstream modules via `import dev.constructive.eo.X`; the move makes
every use site import explicitly. No behaviour change — pure namespacing.
Full suite green, CompositionMatrixSpec still 100/100, benchmarks compile.
Doc snippets (optics.md, multifocus.md) repointed too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Composing a partial optic (
Optional/AffineFold, theAffinecarrier) with a read-onlyGetternow works and yields anAffineFold(a partial read):getOptionthe focus, then read it through the getter.Why a downgrade to
AffineFoldA read-write/partial outer has back-focus
B = A, but aGetter's isUnit— so aGetterinner can never thread through as a writable optic. The read-only downgrade is the only sound result, and it's exactly what you want. It mirrors how aGetterchain composesGetter-to-Getter(a read-only inner collapses the result to read-only).Implementation
Optional(concrete, read-write)andThenGetter— a new fused member on theOptionalclass, alongside its existingandThen(Optional/Lens/Prism/Iso)members, returningAffineFold[S, C]. Deliberately a member, not an extension: an extensionandThenon theAffinereceiver perturbed the existing cross-carrier resolution (it brokeAffine.andThen(MultiFocus)); a member with a distinctDirectGetterparam type is resolved by argument type and leaves every otherandThenuntouched.AffineFold(read-only)andThenGetter— already composes via the morphed path (Morph[Affine, Direct]→AffineFold); now covered by an explicit test.Testing
OpticsBehaviorSpec: a new case asserting both outers (OptionalandAffineFold) compose with aGetterto anAffineFold, hit and miss. 22/22 in that spec.sbt testaggregate green (the change touchesandThenresolution, so the whole optic + law suite was exercised — the previously-passingAffine.andThen(MultiFocus)case still passes).optics.mdunder Optional.Scope
Independent core optic-algebra capability — does not depend on the typed-recursion-schemes work in #24. The
Lens → Gettersibling (read-only downgrade for theTuple2carrier) is the obvious parallel and a natural follow-up; this PR does theAffinecarrier as requested.Post-Deploy Monitoring & Validation
No additional operational monitoring required — pure, additive library change (a new fused
andThenoverload), no runtime/external/deployment impact.🤖 Generated with Claude Code