Std.Optic
View source →Statically-typed optics: lenses, affine traversals, and traversals.
An optic is a first-class, composable "path" into a data structure. Unlike the
deleted Std.Access — which leaned on believe_me and runtime is_map/
is_tuple dispatch over an opaque Any — every optic here is checked by the
dependent kernel. The kind of an optic (how many foci it has, whether focusing
can fail) is a STATIC index k : OpticKind, so view on a lens is total,
preview on an affine returns Option, and a traversal rebuilds a structure
from EXACTLY the foci it produced (a length-indexed Vector, so the rebuild
can never be given the wrong number of elements).
The representation is a single GADT family Optic(s, a, k) — one constructor
per kind, each carrying an extraction function s -> <focus bundle> (the focus
value(s) plus a total rebuilder back to s). Because the kind is a constructor
index, a match on an optic refines k, so the eliminators below are written
once, kind-polymorphically, and stay total at each kind. (The extraction
functions are wrapped in per-kind newtypes because an arrow type does not parse
directly in a GADT indices-form constructor field, only in an inline one.)
Types
-
type OpticKind = LensKind | AffineKind | TraversalKindThe kind of an optic, used as a static index.
LensKindfocuses exactly one value;AffineKindfocuses zero-or-one;TraversalKindfocuses any number. -
type LensRep = MkLensRepA lens focus bundle: the focus value
a, and a total rebuildera -> sthat puts a (possibly new) focus back into the original structure. -
type AffineRep = MkAffineRepAn affine focus bundle: a lens bundle if the focus is present, else nothing (the structure has no focus, e.g. the missing key of a map).
-
type TravRep = MkTravRepA traversal focus bundle: existentially, SOME count
n, aVectorof exactlynfoci, and a rebuilder that consumes exactlynreplacements to rebuild the structure. The length index is what makes the rebuild total — it can only be applied to a vector of the same length it produced. -
type LensExt = MkLensExts -> LensRep(a, s), wrapped. The wrap is only to keep an arrow out of the GADT constructor field below (arrows parse in an inline ctor field, not a GADTindices-form one);MkLensExt/fpeel it back in one step. -
type AffineExt = MkAffineExt -
type TravExt = MkTravExt
Functions
-
# fn Affine(s: Type, a: Type) -> Type
Affine(s, a)— an optic focusing zero-or-oneainsides. -
# fn Lens(s: Type, a: Type) -> Type
Lens(s, a)— an optic focusing exactly oneainsides. -
# fn Traversal(s: Type, a: Type) -> Type
Traversal(s, a)— an optic focusing any number ofas insides. -
# fn affine(s: Type, a: Type, pre: Function(s, Option(a)), put: Function(a, Function(s, s))) -> Optic(s, a, AffineKind)
Build an affine (zero-or-one focus) from a partial getter
pre : s -> Option(a)and a setterput : a -> s -> s. Whenpre(x)isNone,over/setleave the structure untouched andpreviewyieldsNone; this is the total, statically- kinded replacement for anAccess-style "maybe this key exists" path. -
# fn affine_ext(s: Type, a: Type, pre: Function(s, Option(a)), put: Function(a, Function(s, s)), x: s) -> AffineRep(a, s)
The extraction behind
affine, factored out of the constructor lambda so itsmatchbody is an ordinary function (amatchdirectly inside the stored lambda does not lower).pre(x)decides whether the focus is present; when it is, the setterfn(new) -> put(new)(x)is closed overxexactly aslenscloses over its structure. -
# fn affine_to_trav(s: Type, a: Type, o: Optic(s, a, AffineKind)) -> Optic(s, a, TraversalKind)
An affine is a traversal focusing zero-or-one element. Needed so
composecan put an affine where a traversal step is expected (join(AffineKind, TraversalKind) = TraversalKind). -
# fn affine_to_trav_ext(s: Type, a: Type, ar: AffineRep(a, s), x: s) -> TravRep(a, s)
The extraction behind
affine_to_trav(factored out for the samematch-in-a- stored-lambda reason asaffine_ext). A present focus becomes a one-element traversal (Vector(a, S(Z)), rebuilt from the single head); an absent focus becomes the empty traversal (Vector(a, Z), rebuilding to the untouchedx). -
# fn compose(s: Type, a: Type, b: Type, k1: OpticKind, k2: OpticKind, o1: Optic(s, a, k1), o2: Optic(a, b, k2)) -> Optic(s, b, join(k1, k2))
Compose any two optics:
Optic(s, a, k1)afterOptic(a, b, k2)yields anOptic(s, b, join(k1, k2)). Matching both optics refinesk1andk2in the result motive, sojoin(k1, k2)reduces to a concrete kind in every one of the nine branches; each branch widens the laxer-kinded side with a coercion (so both sides meet atjoin(k1, k2)) and delegates to the same-kind composer. This is the one entry point a caller needs —compose_lens/compose_affine/compose_travare the specialised cores it dispatches to. -
# fn compose_affine(s: Type, a: Type, b: Type, o1: Optic(s, a, AffineKind), o2: Optic(a, b, AffineKind)) -> Optic(s, b, AffineKind)
Compose two affines.
join(AffineKind, AffineKind) = AffineKind, so the result is again an affine: it focusesbinsideswhen both partial steps succeed, andpreview/settreat a miss at either step as "no focus" (leavingsuntouched underset). The zero-or-one analogue ofcompose_lens. -
# fn compose_affine_ext(s: Type, a: Type, b: Type, o1: Optic(s, a, AffineKind), o2: Optic(a, b, AffineKind), x: s) -> AffineRep(b, s)
The extraction behind
compose_affine. The focus is present only if BOTH the outer affine finds anainxAND the inner affine finds abin thata; anyNoneshort-circuits toNone. When both hit, the rebuilder threads a newbback through both,ra(rb(new)), exactly as the lens case. -
# fn compose_lens(s: Type, a: Type, b: Type, o1: Optic(s, a, LensKind), o2: Optic(a, b, LensKind)) -> Optic(s, b, LensKind)
Compose two lenses into a lens focusing
binsidesthrough the intermediatea.viewreads through both;set/overrebuild through both. Composing a lens with a lens keeps a lens (join(LensKind, LensKind) = LensKind), so the result is again total — the composability that makes optics worth the GADT. -
# fn compose_lens_ext(s: Type, a: Type, b: Type, o1: Optic(s, a, LensKind), o2: Optic(a, b, LensKind), x: s) -> LensRep(b, s)
The extraction behind
compose_lens, factored out of the constructor lambda (amatchdirectly inside the stored lambda does not lower, exactly as inaffine_ext). Reads the outer lens atxto get focusfa : aand rebuilderra : a -> s, then the inner lens atfato getfb : bandrb : b -> a; the composed rebuilder threads a newbback through both,ra(rb(new)). -
# fn compose_trav(s: Type, a: Type, b: Type, o1: Optic(s, a, TraversalKind), o2: Optic(a, b, TraversalKind)) -> Optic(s, b, TraversalKind)
Compose two traversals into a traversal focusing every
breachable through everya.join(TraversalKind, TraversalKind) = TraversalKind, so the result is again a traversal;to_list_ofon it flattens the full nested collection andover/setrebuild the whole structure from exactly the foci produced. -
# fn compose_trav_ext(s: Type, a: Type, b: Type, o1: Optic(s, a, TraversalKind), o2: Optic(a, b, TraversalKind), x: s) -> TravRep(b, s)
The extraction behind
compose_trav, factored out of the constructor lambda (amatchinside a stored lambda does not lower — same reason asaffine_ext). Reads the outer traversal atxfor its focus vectorvaand structure rebuilderrebuild_s, gathers all inner sub-foci withcompose_trav_gather, and chains the two rebuilders:rebuild_safter the gather'sVector(b, t) -> Vector(a, n), so a length-treplacement vector rebuilds all the way back tos. -
# fn compose_trav_gather(a: Type, b: Type, n: Nat, o2: Optic(a, b, TraversalKind), va: Vector(a, n)) -> Sigma(t: Nat, Sigma(vv: Vector(b, t), Function(Vector(b, t), Vector(a, n))))
The recursion behind
compose_trav. Walks the outer traversal's focus vectorva : Vector(a, n)and, for each focus, runs the inner traversalo2to get aVector(b, m_i)of sub-foci plus a rebuilderVector(b, m_i) -> a. It returns, existentially, the CONCATENATION of every sub-vector (total lengtht) together with a single rebuilderVector(b, t) -> Vector(a, n)that splits a length-tvector back into the per-focus chunks. The split is whereVector.take/dropearn their keep: at each step the combined length isplus(m, t2),take(m, w)peels the current focus'smreplacements anddrop(m, w)leaves the rest — both total precisely because the length index is tracked through theplus. -
# fn dyn_atom() -> Optic(Dynamic, Atom, AffineKind)
-
# fn dyn_atom_pre(d: Dynamic) -> Option(Atom)
One
Affine(Dynamic, T)per constructor ofStd.Dynamic's typed-Anysum. Narrowing aDynamicto a concrete leaf is an AFFINE (it may miss), never a cast:previewyieldsSomeonly when the tag matches, andset/overrebuild a same-taggedDynamic, leaving a mis-tagged value untouched. Each is built from a partial getter (amatchreturningSome/None) and a setter that re-injects through the matching constructor. This is the statically- kinded replacement for a runtimeis_integer/is_mapguess over anAny.The getters are factored out because a
matchdoes not lower as the direct body of the lambdaaffinestores; the setters are constant re-injections (they ignore the old, already-narrowed structure) and inline cleanly. -
# fn dyn_float() -> Optic(Dynamic, Float, AffineKind)
-
# fn dyn_float_pre(d: Dynamic) -> Option(Float)
-
# fn dyn_int() -> Optic(Dynamic, Int, AffineKind)
-
# fn dyn_int_pre(d: Dynamic) -> Option(Int)
-
# fn dyn_list() -> Optic(Dynamic, List(Dynamic), AffineKind)
-
# fn dyn_list_pre(d: Dynamic) -> Option(List(Dynamic))
-
# fn dyn_map() -> Optic(Dynamic, List(DynamicEntry), AffineKind)
-
# fn dyn_map_pre(d: Dynamic) -> Option(List(DynamicEntry))
-
# fn dyn_str() -> Optic(Dynamic, String, AffineKind)
-
# fn dyn_str_pre(d: Dynamic) -> Option(String)
-
# fn dyn_tuple() -> Optic(Dynamic, List(Dynamic), AffineKind)
-
# fn dyn_tuple_pre(d: Dynamic) -> Option(List(Dynamic))
-
# fn first_get_int(x: Tuple(Int, Int)) -> Int
Common tuple lenses exposed through the
lenssyntax macro. The macro is only surface sugar; these are ordinary typed Std.Optic functions. -
# fn first_lens() -> Optic(Tuple(Int, Int), Int, LensKind)
-
# fn first_put_int(y: Int) -> Function(Tuple(Int, Int), Tuple(Int, Int))
-
# fn ix(a: Type, i: Int) -> Optic(List(a), a, AffineKind)
Focus the element at index
iof aList(a). A bareListcarries no length, so the index genuinely may miss —ix(i)is an AFFINE, not a lens:previewyieldsSome(elem)in range andNoneout of range or on[], andset/overreplace in place when the index exists and are a NO-OP otherwise (the affine miss-is-a-no-op law). Totality is available by choosing a length-indexed container instead — aVector(a, n)with aBounded(n)index is a total lens; the container is the totality declaration.The getter and putter are the total
Std.List.at/set_at; both are ordinary calls (not amatch), so they inline as the affine's stored lambdas. -
# fn join(k1: OpticKind, k2: OpticKind) -> OpticKind
The least-upper-bound of two kinds under composition: composing optics of kinds
k1andk2yields an optic of kindjoin(k1, k2). A lens composed with a lens stays a lens; anything touching a traversal becomes a traversal; anything touching an affine (but no traversal) becomes an affine. -
# fn lens(s: Type, a: Type, get: Function(s, a), put: Function(a, Function(s, s))) -> Optic(s, a, LensKind)
Build a lens from a getter and a setter.
get(x)reads the focus;put(y)(x)writesyas the new focus ofx. -
# fn lens_to_affine(s: Type, a: Type, o: Optic(s, a, LensKind)) -> Optic(s, a, AffineKind)
A lens is an affine that always focuses: wrap its single focus bundle in
Some. Needed socomposecan put a lens where an affine step is expected (join(LensKind, AffineKind) = AffineKind). -
# fn lens_to_trav(s: Type, a: Type, o: Optic(s, a, LensKind)) -> Optic(s, a, TraversalKind)
A lens is a one-element traversal — widen through the affine coercion.
-
# fn over(s: Type, a: Type, k: OpticKind, o: Optic(s, a, k), g: Function(a, a), x: s) -> s
Transform every focus of an optic by applying
g, rebuilding the structure. -
# fn preview(s: Type, a: Type, k: OpticKind, o: Optic(s, a, k), x: s) -> Option(a)
The first focus of any optic, or
Noneif it has none. Total at every kind. -
# fn second_get_int(x: Tuple(Int, Int)) -> Int
-
# fn second_lens() -> Optic(Tuple(Int, Int), Int, LensKind)
-
# fn second_put_int(y: Int) -> Function(Tuple(Int, Int), Tuple(Int, Int))
-
# fn set(s: Type, a: Type, k: OpticKind, o: Optic(s, a, k), new: a, x: s) -> s
Replace every focus of an optic with
new.overwith a constant transform: the inlinefn(ignored) -> newignores the old focus. (Its(a) -> adomain is fixed by the laterxargument, so it elaborates in checking mode.) -
# fn to_list_of(s: Type, a: Type, k: OpticKind, o: Optic(s, a, k), x: s) -> List(a)
Every focus of an optic, in order, as an ordinary
List. Total at every kind: a lens yields a one-element list, an affine zero-or-one, a traversal the wholeVectorof foci flattened to a list. The kind-polymorphic counterpart topreview(which keeps only the first). -
# fn view(s: Type, a: Type, o: Optic(s, a, LensKind), x: s) -> a
Read the focus of a lens. Total: a lens always has exactly one focus — this is the concrete win over an
Access-style getter that returnsnil/Option.