The adapt Primitive
Leanage
Section titled “Leanage”The adapt combinator was inspired on denful/fx-rs kernel, where adapt is the basis for lens-based effect-rows.
And later ported to Nix on denful/nfx.
No other combinator in Bend is axiomatic. Every function in bend.lib is derived from adapt.
The signature
Section titled “The signature”adapt lens from back refineFour arguments:
| Argument | Type | Role |
|---|---|---|
lens | { get; set } | inner lens to delegate to |
from | outer → Either inner | extract inner source from outer |
back | outer → inner → Either outer | write inner result back into outer |
refine | inner → Either result | transform the focused value |
adapt returns a new lens { get; set } over the outer type.
How it works
Section titled “How it works”get outer:
from outer— extract inner source. Returnsright innerorleft outer.lens.get inner— run inner lens. Returnsright valueorleft inner.refine value— transform focused value. Returnsright resultorleft.
Any step returning left short-circuits; the left propagates unchanged.
set outer value:
from outer— extract inner source (same as get).lens.set inner value— write into inner. Returnsright inner'orleft.back outer inner'— write inner result back into outer. Returnsright outer'orleft.
The symmetry between get and set is what makes every adapt-derived lens bidirectional by construction.
Why four arguments is enough
Section titled “Why four arguments is enough”A lens is fundamentally: focus → read → write → transform. Those are exactly the four roles. Any more arguments would be redundant; any fewer would lose expressiveness.
Consider what each role provides:
lens— reuse existing lenses (composition)from— navigate to a substructure (zoom in)back— reconstruct the whole from a modified part (zoom out)refine— validate or transform the value at focus (parse)
Remove refine and you can’t parse. Remove back and you can’t write. Remove from and you can’t navigate. Remove lens and you can’t compose. All four are load-bearing.
Deriving everything from adapt
Section titled “Deriving everything from adapt”identity
Section titled “identity”identity = { get = bend.right; set = _: bend.right;};Not built with adapt directly — it is the base case. adapt delegates to identity when no inner lens is needed.
attr "name"
Section titled “attr "name"”attr name = adapt identity (s: if s ? ${name} then right s.${name} else left s) (s: v: right (s // { ${name} = v; })) right;from extracts the field or short-circuits. back merges the new value back. refine = right (no further transformation).
parse refine lens
Section titled “parse refine lens”parse refine lens = adapt lens right (_: v: v) refine;from = right (identity extraction). back discards the source — parse results don’t write back through the source, they produce a new value. refine does the parsing work.
compose outer inner
Section titled “compose outer inner”compose outer inner = adapt inner outer.get outer.set right;from = outer.get (navigate with the outer lens). back = outer.set (write back with outer). refine = right (no extra transformation). The inner lens handles its own navigation.
pipe [l₁ l₂ ... lₙ]
Section titled “pipe [l₁ l₂ ... lₙ]”Fold compose over the list:
pipe steps = foldl compose identity steps;record validators
Section titled “record validators”Each field is an attr lens composed with its validator. The results are sequenced. Short-circuit on first failure.
each lens
Section titled “each lens”Map lens.get over every element. If any returns left, return left [all results]. Otherwise right [all rights].
set maps lens.get vs[i] over new values (validating them), same evidence structure on failure.
The power of a single primitive
Section titled “The power of a single primitive”Having one primitive instead of many means:
- No special cases — every lens obeys the same contract.
- Composition is free — any two lenses compose via
compose/pipewithout adapter code. - Bidirectionality is structural —
setis derived from the samefrom/backpair asget. You cannot write a lens that reads one path and writes another. - Testing is uniform — the same test pattern (
get,set, roundtrip) applies to every lens in the library.