Skip to content

Bend

Lens based data parser-combinators. Bidirectional transformation and validation pipelines for Nix.

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.

Everything in Bend composes from a single combinator:

adapt from back refine lens
# from — extract inner source from outer
# back — write inner result back into outer
# refine — refine focused value
# lens — inner lens

right 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 != ""))
];
in
lens.get { name = "alice"; } # right "alice"
lens.get { name = ""; } # left ""
lens.get { name = 42; } # left 42
lens.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")
];
in
lens.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. Bidirectional: validates on both get and set.

(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.each bend.int).set [ 1 2 3 ] [ 10 20 30 ]
# right [ 10 20 30 ]
(bend.each bend.int).set [ 1 2 3 ] [ 10 "x" 30 ]
# left [ (right 10) (left "x") (right 30) ]
(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

alt tries a second lens when the first fails. recover applies a function to the error.

let lens = bend.alt
(bend.attr "preferred")
(bend.attr "fallback");
in
lens.get { fallback = "value"; } # right "value"
let lens = bend.recover
(e: bend.right "default")
(bend.attr "name");
in
lens.get { } # right "default"

label replaces errors with custom messages. ensure adds post-parse validation. annotate tracks context.

let lens = bend.pipe [
(bend.label "age field missing" (bend.attr "age"))
(bend.ensure (n: n > 0) "age must be positive")
];
in
lens.get { } # left "age field missing"
lens.get { age = -5 } # left "age must be positive"

when and unless apply a lens only if a condition holds. option provides a default on failure.

let lens = bend.when (s: s ? admin) (bend.attr "admin");
in
lens.get { admin = true; } # right true
lens.get { } # right { } — unchanged
let lens = bend.option "guest" (bend.attr "user");
in
lens.get { } # right "guest" — default on missing

path chains attr lenses for nested data. index accesses list elements by position.

let lens = bend.path [ "config" "server" "port" ];
in
lens.get { config = { server = { port = 8080; }; }; }
# right 8080
(bend.index 1).get [ "a" "b" "c" ] # right "b"
(bend.index 5).get [ "a" "b" ] # left [ "a" "b" ]

map transforms a lens result. bimap transforms both left and right. rmap/lmap transform one side.

let lens = bend.map (x: x * 2) (bend.attr "count");
in
lens.get { count = 5 } # right 10
let lens = bend.rmap (s: "got: ${s}") bend.str;
in
lens.get "hello" # right "got: hello"
Combinators Reference
Contribute Community Sponsor