final class Postpro[F[_], A, S](val eta: FunctionK[F, F], val coalg: A=>F[A])(using F: Traverse[F], P: Project[F, S], E: Embed[F, S]) extends BuildScheme[S, A]
Postpromorphism citizen — an unfold that applies a natural transformation η : F ~> F after each step (BuildScheme). The build-side mirror of Prepro: the coalgebra coalg: A => F[A] is exactly Ana's (so X = S, the structure it threads), but each emitted subtree is hoisted through η once per level it sits below the root.
Like Prepro, this is the layer-transforming axis, not an index refinement: apo/futu refine the residual X; postpro keeps ana's index and decorates the layer optic on the embed glue side. With η = id it is exactly Ana.
'''Cost, honestly.''' Each built child subtree is hoisted (embed ∘ η at every layer) before its parent embeds it, so a node at depth k is re-transformed k times: O(n · depth), the inherent cost of the postpromorphism. Both the outer build and each hoist run on the stack-safe Machines.foldLayered machine.
Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrierF. Flip self (it must be reversible — Accessor[F]andReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThenthat under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.
Build-then-observe across the build-output ⇄ read-input seam, preserving structure, on a shared carrierF. Flip self (it must be reversible — Accessor[F]andReverseAccessor[F], i.e. an Iso or Review over Direct) so it reads T from B, then andThenthat under the same carrier. The result is the full Optic[B, A, C, D, F], not a collapsed getter: its read capability follows the carrier (.get for Direct), and self's read focus A survives as the composite's write-back focus.
This is exactly self.reverse.andThen(that). The motivating case is ana.cross(cata): a Review (the unfold) crossed with a getter on the built S (the fold) — a (materializing) hylomorphism whose .get reads the folded value. When that sits on a different carrier (a Prism, a Fold, …), the cross-carrier cross overload below is selected instead.
Seam: that's source is self's T and its back-type is self's S.
Existential leftover carried alongside the focus — the type-level witness the carrier uses to rebuild T. Concrete at construction (Lens.apply sets X = S, Prism.apply sets X = S, …) and abstract when the optic is bound to Optic[…, F] without refinement.
Existential leftover carried alongside the focus — the type-level witness the carrier uses to rebuild T. Concrete at construction (Lens.apply sets X = S, Prism.apply sets X = S, …) and abstract when the optic is bound to Optic[…, F] without refinement.
ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).
ANY outer ∘ read-only inner — the inner is honestly one-way (T = Unit: a Getter, AffineFold, or Fold), so only the two READ sides matter and the composite collapses to the read-only join of their strengths via compose.ReadCompose (Getter / PickFold / ForgetFold).
A trait member (not an extension in the companion) deliberately: once a receiver is statically one of the fused concrete classes, its andThen member overloads enter resolution and Scala 3 never falls back to extension methods when they all fail — the collapse must be in the member overload set to be reachable without an expected-type ascription.
Only the inner's T is pinned to Unit; its B stays free (IB) even though read-only inners always have B = Unit. That keeps this overload strictly LESS specific than the same-carrier andThen above (which accepts every argument this one does whenever B = Unit at the receiver), so a same-carrier read-only ∘ read-only call resolves unambiguously to the AssociativeFunctor path and this one fires exactly on the cross-seam cells the generic member cannot type.
Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.
Compose with another optic under the shared carrier F. Requires AssociativeFunctor[F]. Cross-carrier composition (Lens → Optional, Lens → Traversal, …) goes through the Morph-summoning overload of this same method.
Attributes
Example
case class Address(street: String)
case class Person(address: Address)
val streetLens = lens[Person](_.address).andThen(lens[Address](_.street))