Parse, Don't Validate
A lens always produce refined data on left/right.
Parse, Don't Validate
A lens always produce refined data on left/right.
Bidirectional
Every lens reads and writes through the same path. get parses; set reconstructs.
Composable
pipe, record, alt — and parser-combinators inspired operations compose freely from a single adapt primitive.
All-or-Nothing
Collection combinators either succeed entirely or return structured evidence of every failure.
adapt primitiveEverything in Bend composes from a single combinator:
adapt lens from back refine# lens — inner lens# from — extract inner source from outer# back — write inner result back into outer# refine — refine focused valueright and left, both carry data not only booleans or error strings.
bend.right 42 # { right = 42; }bend.left "hello" # { left = "hello"; }
bend.mapR (x: x + 1) (bend.right 5) # { right = 6; }bend.mapL (x: x + 1) (bend.left 5) # { left = 6; }bend.chain (x: bend.right (x * 2)) (bend.right 5) # { right = 10; }bend.swap (bend.right 1) # { left = 1; }A validators return refined, structured data perserve validation proofs.
bend.nonEmpty.get [ 1 2 3 ] # right { head = 1; tail = [ 2 3 ]; }bend.nonEmpty.get [ ] # left [ ]compose threads two lenses. pipe composes a list left-to-right. Each step either refines or short-circuits with the point of failure.
let lens = bend.pipe [ (bend.attr "name") bend.str (bend.satisfy (s: s != ""))];inlens.get { name = "alice"; } # right "alice"lens.get { name = ""; } # left ""lens.get { name = 42; } # left 42lens.get { } # left { }Lenses write back through the same path they read. set returns right on success or left if the path cannot be reached.
let lens = bend.pipe [ (bend.attr "config") (bend.attr "timeout")];inlens.get { config = { timeout = 30; }; }# right 30
lens.set { config = { timeout = 30; retry = 3; }; extra = true; } 60# right { config = { timeout = 60; retry = 3; }; extra = true; }attr returns left on set when the key is absent — you cannot write through a missing path.
record validates each field and short-circuits on the first failure. recordAll gathers all failures.
(bend.record { name = bend.str; age = bend.int;}).get { name = "alice"; age = 30; extra = true; }# right { name = "alice"; age = 30; }
(bend.recordAll { name = bend.str; age = bend.int;}).get { name = "alice"; age = "thirty"; }# left {# name = right "alice";# age = left { field = "age"; got = "thirty"; };# }each applies a lens to every element and returns per-element proof on failure:
(bend.each bend.int).get [ 1 2 3 ]# right [ 1 2 3 ]
(bend.each bend.int).get [ 1 "x" 3 ]# left [ (right 1) (left "x") (right 3) ](bend.many bend.int).get [ ] # right [] — zero or more(bend.some bend.int).get [ ] # left [] — one or more required(bend.atLeast 2 bend.int).get [ 1 ] # left [1] — at least 2 required(bend.exactly 3 bend.int).get [ 1 2 ] # left [1 2] — exactly 3 required