Partial lenses is a comprehensive, high-performance optics library for JavaScript
npm install partial.lensesLenses are basically an abstraction for simultaneously specifying operations to
update and query immutable data
structures. Lenses are highly composable and can be
efficient. This library provides a rich
collection of partial
isomorphisms, lenses, and traversals,
collectively known as optics, for manipulating
JSON and users can write
new optics for manipulating non-JSON objects, such as
Immutable.js collections. A partial lens can view optional
data, insert new data, update existing data and remove existing data and
can, for example, provide defaults and maintain required data structure
parts. Try Lenses!






* Tutorial
* Getting started
* A partial lens to access title texts
* Querying data
* Missing data can be expected
* Updating data
* Inserting data
* Removing data
* Exercises
* Shorthands
* Systematic decomposition
* Manipulating multiple items
* Next steps
* The why of optics
* Reference
* Stable subset
* Additional libraries
* Optics
* On partiality
* On indexing
* On immutability
* On composability
* On lens laws
* Myth: Partial Lenses are not lawful
* Operations on optics
* {p1: a1, ...ps} -> Maybe s -> Maybe s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.assign(optic, object, maybeData) ~> maybeData v11.13.0
* [L.disperse(optic, [...maybeValues], maybeData) ~> maybeData](#l-disperse "L.disperse: POptic s a -> Maybe [Maybe a] -> Maybe s -> Maybe s") v14.6.0
* ((Maybe a, Index" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData -> Maybe a) -> Maybe s -> Maybe s") v2.2.0
* ((Maybe a, Index" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.modifyAsync(optic, (maybeValue, index) => maybeValuePromise, maybeData) ~> maybeDataPromise -> Promise (Maybe a)) -> Maybe s -> Promise (Maybe s)") v13.12.0
* Maybe s -> Maybe s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.remove(optic, maybeData) ~> maybeData v2.0.0
* Maybe a -> Maybe s -> Maybe s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.set(optic, maybeValue, maybeData) ~> maybeData v1.0.0
* L.traverse(algebra, (maybeValue, index) => operation, optic, maybeData) ~> operation c -> ((Maybe a, Index) -> c b) -> POptic s t a b -> Maybe s -> c t") v10.0.0
* Nesting
* L.compose(...optics) ~> optic -> POptic s a") or [...optics] v1.0.0
* L.flat(...optics) ~> optic -> POptic [...s...] a") v13.6.0
* Recursing
* POptic s a" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.lazy(optic => optic) ~> optic -> POptic s a") v5.1.0
* Adapting
* L.choices(optic, ...optics) ~> optic -> POptic s a") v11.10.0
* L.choose((maybeValue, index) => optic) ~> optic -> POptic s a) -> POptic s a") v1.0.0
* L.cond(...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic v13.1.0
* L.condOf(traversal, ...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic v13.5.0
* L.ifElse((maybeValue, index) => testable, optic, optic) ~> optic -> Boolean) -> POptic s a -> POptic s a -> POptic s a") v13.1.0
* L.orElse(backupOptic, primaryOptic) ~> optic -> POptic s a") v2.1.0
* Indices
* POptic s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.joinIx(optic) ~> optic v13.15.0
* L.mapIx((index, maybeValue) => index) ~> optic -> Index) -> POptic a a") v13.15.0
* POptic s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.reIx(optic) ~> optic v14.10.0
* POptic a a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.setIx(index) ~> optic v13.15.0
* POptic s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.skipIx(optic) ~> optic v13.15.0
* L.tieIx((innerIndex, outerIndex) => index, optic) ~> optic => Index) -> POptic s a -> POptic s a") v13.15.0
* Debugging
* Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.getLog(lens, maybeData) ~> maybeValue v13.14.0
* L.log(...labels) ~> optic -> POptic s s") v3.2.0
* Internals
* L.Identity ~> Monad v13.7.0
* L.IdentityAsync ~> Monadish v13.12.0
* L.Select ~> Applicative v14.0.0
* (Maybe s, Index, (Functor|Applicative|Monad" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.toFunction(optic) ~> optic c, (Maybe a, Index) -> c b) -> c t") v7.0.0
* Transforms
* Operations on transforms
* Maybe s -> Maybe s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.transform(optic, maybeData) ~> maybeData v11.7.0
* Maybe s -> Promise (Maybe s" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.transformAsync(optic, maybeData) ~> maybeDataPromise") v13.12.0
* Sequencing
* L.seq(...transforms) ~> transform -> PTransform s a") v9.4.0
* Transforming
* PTraversal [a] a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.appendOp(value) ~> traversal v14.14.0
* PTraversal {p1: a1, ...ps, ...o} {p1: a1, ...ps}"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.assignOp(object) ~> traversal v11.13.0
* L.modifyOp((maybeValue, index) => maybeValue) ~> traversal -> Maybe a) -> PTraversal a a") v11.7.0
* PTraversal [a] a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.prependOp(value) ~> traversal v14.14.0
* L.removeOp ~> traversal v11.7.0
* PTraversal a a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.setOp(maybeValue) ~> traversal v11.7.0
* Traversals
* Creating new traversals
* PTraversal {p1: p1, ...ps} a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.branch({prop: traversal, ...props}) ~> traversal v5.1.0
* {p1: PTraversal p1 a, ...pts} -> PTraversal {p1: p1, ...ps} a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.branchOr(traversal, {prop: traversal, ...props}) ~> traversal v13.2.0
* L.branches(...propNames) ~> traversal -> PTraversal {p1: p1, ...ps} a") v13.5.0
* Traversals and combinators
* L.children ~> traversal a") v13.3.0
* L.elems ~> traversal v7.3.0
* L.elemsTotal ~> traversal v13.11.0
* L.entries ~> traversal v11.21.0
* L.flatten ~> traversal v11.16.0
* L.keys ~> traversal v11.21.0
* L.keysEverywhere ~> traversal v14.12.0
* L.leafs ~> traversal") v13.3.0
* PTraversal s a -> PTraversal s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.limit(count, traversal) ~> traversal v14.10.0
* PTraversal String String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.matches(/.../g) ~> traversal v10.4.0
* PTraversal s a -> PTraversal s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.offset(count, traversal) ~> traversal v14.10.0
* L.query(...traversals) ~> traversal ~> PTraversal JSON a") v13.6.0
* L.satisfying((maybeValue, index) => testable) ~> traversal -> Boolean) -> PTraversal JSON a") v13.3.0
* Integer -> PTraversal s a -> PTraversal s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.subseq(begin, end, traversal) ~> traversal v14.10.0
* L.values ~> traversal v7.3.0
* PTraversal JSON {p1: p1, ...ps}"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.whereEq({prop: value, ...props}) ~> traversal v14.16.0
* Querying
* L.chain((value, index) => optic, optic) ~> traversal -> POptic s b) -> POptic s a -> PTraversal s b") v3.1.0
* L.choice(...optics) ~> traversal -> PTraversal s a") v2.1.0
* L.optional ~> traversal v3.7.0
* L.unless((maybeValue, index) => testable) ~> traversal -> Boolean) -> PTraversal a a") v12.1.0
* L.when((maybeValue, index) => testable) ~> traversal -> Boolean) -> PTraversal a a") v5.2.0
* L.zero ~> traversal v6.0.0
* Folds over traversals
* L.all((maybeValue, index) => testable, traversal, maybeData) ~> boolean -> Boolean) -> PTraversal s a -> Boolean") v9.6.0
* L.all1((maybeValue, index) => testable, traversal, maybeData) ~> boolean -> Boolean) -> PTraversal s a -> Boolean") v14.4.0
* Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.and(traversal, maybeData) ~> boolean v9.6.0
* Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.and1(traversal, maybeData) ~> boolean v14.4.0
* L.any((maybeValue, index) => testable, traversal, maybeData) ~> boolean -> Boolean) -> PTraversal s a -> Boolean") v9.6.0
* [L.collect(traversal, maybeData) ~> [...values]](#l-collect "L.collect: PTraversal s a -> Maybe s -> [a]") v3.6.0
* [L.collectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...values]](#l-collectas "L.collectAs: ((Maybe a, Index) -> Maybe b) -> PTraversal s a -> Maybe s -> [b]") v7.2.0
* [L.collectTotal(traversal, maybeData) ~> [...maybeValues]](#l-collecttotal "L.collectTotal: PTraversal s a -> Maybe s -> [Maybe a]") v14.6.0
* [L.collectTotalAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...maybeValues]](#l-collecttotalas "L.collectTotalAs: ((Maybe a, Index) -> Maybe b) -> PTraversal s a -> Maybe s -> [Maybe b]") v14.6.0
* (PTraversal s a -> Maybe s -> a" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.concat(monoid, traversal, maybeData) ~> value") v7.2.0
* L.concatAs((maybeValue, index) => value, monoid, traversal, maybeData) ~> value -> r) -> Monoid r -> (PTraversal s a -> Maybe s -> r)") v7.2.0
* Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.count(traversal, maybeData) ~> number v9.7.0
* L.countIf((maybeValue, index) => testable, traversal, maybeData) ~> number -> Boolean) -> PTraversal s a -> Number") v11.2.0
* Map Any Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.counts(traversal, maybeData) ~> map v11.21.0
* L.countsAs((maybeValue, index) => any, traversal, maybeData) ~> map -> Any) -> PTraversal s a -> Map Any Number") v11.21.0
* L.foldl((value, maybeValue, index) => value, value, traversal, maybeData) ~> value -> r) -> r -> PTraversal s a -> Maybe s -> r") v7.2.0
* L.foldr((value, maybeValue, index) => value, value, traversal, maybeData) ~> value -> r) -> r -> PTraversal s a -> Maybe s -> r") v7.2.0
* L.forEach((maybeValue, index) => undefined, traversal, maybeData) ~> undefined -> Undefined) -> PTraversal s a -> Maybe s -> Undefined") v11.20.0
* L.forEachWith(() => context, (context, maybeValue, index) => undefined, traversal, maybeData) ~> context -> c) -> ((c, Maybe a, Index) -> Undefined) -> PTraversal s a -> Maybe s -> c") v13.4.0
* Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.get(traversal, maybeData) ~> maybeValue v2.2.0
* L.getAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue -> Maybe b) -> PTraversal s a -> Maybe s -> Maybe b") v14.0.0
* Maybe s -> Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.isDefined(traversal, maybeData) ~> boolean v11.8.0
* Maybe s -> Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.isEmpty(traversal, maybeData) ~> boolean v11.5.0
* PTraversal s a -> Maybe s -> String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.join(string, traversal, maybeData) ~> string v11.2.0
* L.joinAs((maybeValue, index) => maybeString, string, traversal, maybeData) ~> string -> Maybe String) -> String -> PTraversal s a -> Maybe s -> String") v11.2.0
* PTraversal s a -> Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.maximum(traversal, maybeData) ~> maybeValue v7.2.0
* (PLens a k -> PTraversal s a -> Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.maximumBy(keyLens, traversal, maybeData) ~> maybeValue v11.2.0
* Maybe s -> Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.mean(traversal, maybeData) ~> number v11.17.0
* L.meanAs((maybeValue, index) => maybeNumber, traversal, maybeData) ~> number -> Maybe Number) -> PTraversal s a -> Maybe s -> Number") v11.17.0
* PTraversal s a -> Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.minimum(traversal, maybeData) ~> maybeValue v7.2.0
* (PLens a k -> PTraversal s a -> Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.minimumBy(keyLens, traversal, maybeData) ~> maybeValue v11.2.0
* L.none((maybeValue, index) => testable, traversal, maybeData) ~> boolean -> Boolean) -> PTraversal s a -> Boolean") v11.6.0
* Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.or(traversal, maybeData) ~> boolean v9.6.0
* Maybe s -> Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.product(traversal, maybeData) ~> number v7.2.0
* L.productAs((maybeValue, index) => number, traversal, maybeData) ~> number -> Number) -> PTraversal s a -> Maybe s -> Number") v11.2.0
* ~~ Maybe s -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.select(traversal, maybeData) ~> maybeValue v9.8.0~~
* ~~L.selectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue -> Maybe b) -> PTraversal s a -> Maybe s -> Maybe b") v9.8.0~~
* Maybe s -> Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.sum(traversal, maybeData) ~> number v7.2.0
* L.sumAs((maybeValue, index) => number, traversal, maybeData) ~> number -> Number) -> PTraversal s a -> Maybe s -> Number") v11.2.0
* Lenses
* Creating new lenses
* Maybe s -> Maybe a" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.foldTraversalLens((traversal, maybeData) => maybeValue, traversal) ~> lens -> PTraversal s a -> PLens s a") v11.5.0
* L.getter((maybeData, index) => maybeValue) ~> lens -> Maybe a) -> PLens s a") v13.16.0
* L.lens((maybeData, index) => maybeValue, (maybeValue, maybeData, index) => maybeData) ~> lens -> Maybe a) -> ((Maybe a, Maybe s, Index) -> Maybe s) -> PLens s a") v1.0.0
* L.partsOf(traversal, ...traversals) ~> lens -> PLens s [Maybe a]") v14.6.0
* L.setter((maybeValue, maybeData, index) => maybeData) ~> lens -> Maybe s) -> PLens s a") v10.3.0
* Enforcing invariants
* PLens s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.defaults(valueIn) ~> lens v2.0.0
* PLens s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.define(value) ~> lens v1.0.0
* L.normalize((value, index) => maybeValue) ~> lens -> Maybe s) -> PLens s s") v1.0.0
* PLens s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.required(valueOut) ~> lens v1.0.0
* L.reread((valueIn, index) => maybeValueIn) ~> lens -> Maybe s) -> PLens s s") v11.21.0
* L.rewrite((valueOut, index) => maybeValueOut) ~> lens -> Maybe s) -> PLens s s") v5.1.0
* Lensing array-like objects
* ~~L.append ~> lens v1.0.0~~
* [L.cross([...lenses]) ~> lens](#l-cross "L.cross: [PLens s1 a1, ...PLens sN aN] -> PLens [s1, ...sN] [a1, ...aN]") v14.3.0
* L.filter((maybeValue, index) => testable) ~> lens -> Boolean) -> PLens [a] [a]") v1.0.0
* [L.find((maybeValue, index, {hint: index}) => testable[, {hint: index}]) ~> lens](#l-find "L.find: ((Maybe a, Index, {hint: Index}) -> Boolean[, {hint: Index}]) -> PLens [a] a") v1.0.0
* [L.findWith(optic[, {hint: index}]) ~> optic](#l-findwith "L.findWith: (POptic s a[, {hint: Index}]) -> POptic [s] a") v1.0.0
* L.first ~> lens v13.1.0
* PLens [a] a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.index(elemIndex) ~> lens or elemIndex v1.0.0
* L.last ~> lens v9.8.0
* PLens [a] [a]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.prefix(maybeEnd) ~> lens v11.12.0
* Maybe Number -> PLens [a] [a]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.slice(maybeBegin, maybeEnd) ~> lens v8.1.0
* PLens [a] [a]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.suffix(maybeBegin) ~> lens v11.12.0
* Lensing objects
* PLens {p1: s1, ...pls} {p1: a1, ...pls}"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.pickIn({prop: lens, ...props}) ~> lens v11.11.0
* L.prop(propName) ~> lens -> PLens {p: a, ...ps} a") or propName v1.0.0
* L.props(...propNames) ~> lens -> PLens {p1: a1, ...ps, ...o} {p1: a1, ...ps}") v1.4.0
* L.propsExcept(...propNames) ~> lens -> PLens {p1: a1, ...ps, ...o} {...o}") v14.11.0
* ~~ PLens {p1: a1, ...ps, ...o} {p1: a1, ...ps}"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.propsOf(object) ~> lens v11.13.0~~
* L.removable(...propNames) ~> lens -> PLens {p1: a1, ...ps, ...o} {p1: a1, ...ps, ...o}") v9.2.0
* Lensing strings
* PLens String String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.matches(/.../) ~> lens v10.4.0
* Providing defaults
* PLens s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.valueOr(valueOut) ~> lens v3.5.0
* Transforming data
* PLens s {p1: a1, ...pls}"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.pick({prop: lens, ...props}) ~> lens v1.2.0
* Maybe s -> PLens s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.replace(maybeValueIn, maybeValueOut) ~> lens v1.0.0
* Inserters
* L.appendTo ~> lens v14.14.0
* L.assignTo ~> lens v14.14.0
* L.prependTo ~> lens v14.14.0
* Isomorphisms
* Operations on isomorphisms
* Maybe b -> Maybe a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.getInverse(isomorphism, maybeData) ~> maybeData v5.0.0
* Creating new isomorphisms
* Maybe a" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.iso(maybeData => maybeValue, maybeValue => maybeData) ~> isomorphism -> (Maybe a -> Maybe s) -> PIso s a") v5.3.0
* [L.mapping([patternFwd, patternBwd] | (...variables) => [patternFwd, patternBwd]) ~> isomorphism](#l-mapping "L.mapping: ([Pattern s, Pattern a] | (...Variable) -> [Pattern s, Pattern a]) -> PIso s a") v14.8.0
* L._ ~> pattern v14.8.0
* [L.mappings([...[patternFwd, patternBwd]] | (...variables) => [...[patternFwd, patternBwd]]) ~> isomorphism](#l-mappings "L.mappings: ([[Pattern s, Pattern a]] | (...Variable) -> [[Pattern s, Pattern a]]) -> PIso s a") v14.8.0
* L.pattern(pattern | (...variables) => pattern) ~> isomorphism -> Pattern s) -> PIso s s") v14.13.0
* [L.patterns([...patterns] | (...variables) => [...patterns]) ~> isomorphism](#l-patterns "L.patterns: ([Pattern s] | (...Variable) -> [Pattern s]) -> PIso s s") v14.13.0
* Isomorphism combinators
* L.alternatives(isomorphism, ...isomorphisms) ~> isomorphism -> PIso s a") v14.7.0
* L.applyAt(elementsOptic, isomorphism) ~> isomorphism -> PIso s s") v14.9.0
* L.attemptEveryDown(isomorphism) ~> isomorphism -> PIso s t") v14.13.0
* L.attemptEveryUp(isomorphism) ~> isomorphism -> PIso s t") v14.13.0
* L.attemptSomeDown(isomorphism) ~> isomorphism -> PIso s t") v14.13.0
* PIso a a -> PIso s s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.conjugate(contextIsomorphism, isomorphism) ~> isomorphism v14.9.0
* PIso [s, xs] s"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.fold(isomorphism) ~> isomorphism v14.13.0
* PIso b a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.inverse(isomorphism) ~> isomorphism v4.1.0
* PIso a a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.iterate(isomorphism) ~> isomorphism v14.3.0
* L.orAlternatively(backupIsomorphism, primaryIsomorphism) ~> isomorphism -> PIso s a") v14.7.0
* PIso s [s, xs]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.unfold(isomorphism) ~> isomorphism v14.13.0
* Basic isomorphisms
* L.complement ~> isomorphism v9.7.0
* L.identity ~> isomorphism v1.3.0
* PIso v Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.is(value) ~> isomorphism v11.1.0
* Boolean" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.subset(maybeValue => testable) ~> isomorphism -> PIso a a") v14.3.0
* Array isomorphisms
* PIso [a] [b]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.array(isomorphism) ~> isomorphism v11.19.0
* PIso [a] [b]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.arrays(isomorphism) ~> isomorphism v14.13.0
* PIso [a] [[a]]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.groupBy(keyLens) ~> isomorphism v14.13.0
* L.indexed ~> isomorphism v11.21.0
* L.reverse ~> isomorphism v11.22.0
* L.singleton ~> isomorphism v11.18.0
* PIso [[a]] [a]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.ungroupBy(keyLens) ~> isomorphism v14.13.0
* PIso [c] [a, [b]]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.unzipWith1(isomorphism) ~> isomorphism v14.13.0
* PIso [a, [b]] [c]"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.zipWith1(isomorphism) ~> isomorphism v14.13.0
* Object isomorphisms
* String g" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.disjoint(propName => propName) ~> isomorphism -> PIso {[k]: a} {[g]: {[k]: a}}") v13.13.0
* L.keyed ~> isomorphism v11.21.0
* L.multikeyed ~> isomorphism v14.1.0
* Standard isomorphisms
* PIso String JSON"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.json({reviver, replacer, space}) ~> isomorphism v11.3.0
* L.uri ~> isomorphism v11.3.0
* L.uriComponent ~> isomorphism") v11.3.0
* Standardish isomorphisms
* L.querystring ~> isomorphism v14.2.0
* String isomorphisms
* PIso String String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.dropPrefix(prefix) ~> isomorphism v13.8.0
* PIso String String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.dropSuffix(suffix) ~> isomorphism v13.8.0
* String -> PIso String String"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.replaces(substringIn, substringOut) ~> isomorphism v13.8.0
* [L.split(separator[, separatorRegExp]) ~> isomorphism](#l-split "L.split: (String[, String | RegExp]) -> PIso String [String]") v13.8.0
* [L.uncouple(separator[, separatorRegExp]) ~> isomorphism](#l-uncouple "L.uncouple: (String[, String | RegExp]) -> PIso String [String, String]") v13.8.0
* Arithmetic isomorphisms
* PIso Number Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.add(number) ~> isomorphism v13.9.0
* PIso Number Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.divide(number) ~> isomorphism v13.9.0
* PIso Number Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.multiply(number) ~> isomorphism v13.9.0
* L.negate ~> isomorphism v13.9.0
* PIso Number Number"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.subtract(number) ~> isomorphism v13.9.0
* Interop
* Fantasy Land
* L.FantasyFunctor ~> Functor v14.5.0
* Functor|Applicative|Monad"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.fromFantasy(TypeRep) ~> Functor|Applicative|Monad v14.5.0
* Applicative"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.fromFantasyApplicative(TypeRep) ~> Applicative v14.5.0
* Monad"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.fromFantasyMonad(TypeRep) ~> Monad v14.5.0
* JSON Pointer
* PLens s a"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.pointer(jsonPointer) ~> lens v11.21.0
* Auxiliary
* Boolean"" class="text-primary hover:underline" target="_blank" rel="noopener noreferrer">L.seemsArrayLike(anything) ~> boolean v11.4.0
* Examples
* An array of ids as boolean flags
* Dependent fields
* Collection toggle
* BST as a lens
* BST traversal
* Interfacing with Immutable.js
* List indexing
* Interfacing traversals
* Deepening topics
* Understanding L.filter, L.find, L.get, and L.when
* Advanced topics
* Performance tips
* Nesting traversals does not create intermediate aggregates
* Avoid reallocating optics in L.choose
* On bundle size and minification
* Background
* Motivation
* Design choices
* Partiality
* Focus on JSON
* Use of undefined
* Allowing strings and integers as optics
* Treating an array of optics as a composition of optics
* Applicatives
* Combinators for creating new optics
* Indexing
* Static Land
* Performance
* Benchmarks
* Lenses all the way
* Related work
* Papers and other introductory material
* JavaScript / TypeScript / Flow libraries
* Libraries for other languages
* Contributing
* Building
* Testing
* Documentation
Let's look at an example that is based on an actual early use case that lead to
the development of this library. What we have is an external HTTP API that both
produces and consumes JSON objects that include, among many other properties, atitles property:
``js`
const sampleTitles = {
titles: [
{language: 'en', text: 'Title'},
{language: 'sv', text: 'Rubrik'}
]
}
We ultimately want to present the user with a rich enough editor, with features
such as undo-redo and
validation, for
manipulating the content represented by those JSON objects. The titles
property is really just one tiny part of the data model, but, in this tutorial,
we only look at it, because it is sufficient for introducing most of the basic
ideas.
So, what we'd like to have is a way to access the text of titles in a given
language. Given a language, we want to be able to
* get the corresponding text,
* update the corresponding text,
* insert a new text and the immediately surrounding object in a new language, and
* remove an existing text and the immediately surrounding object.
Furthermore, when updating, inserting, and removing texts, we'd like the
operations to treat the JSON as immutable and create new
JSON objects with the changes rather than mutate existing JSON objects, because
this makes it trivial to support features such as undo-redo and can also help to
avoid bugs associated with mutable state.
Operations like these are what lenses are good at. Lenses can be seen as a
simple embedded DSL
for specifying data manipulation and querying functions. Lenses allow you to
focus on an element in a data structure by specifying a path from the root of
the data structure to the desired element. Given a lens, one can then perform
operations, like get and set, on the element that the
lens focuses on.
Let's first import the libraries
`jsx`
import * as L from 'partial.lenses'
import * as R from 'ramda'
and ▶ play just a
bit with lenses.
> Note that links with the ▶
> play symbol, take
> you to an interactive version of this page where almost all of the code
> snippets are editable and evaluated in the browser. There is also a separate
> playground page
> that allows you to quickly try out lenses.
As mentioned earlier, with lenses we can specify a path to focus on an element.
To specify such a path we use primitive lenses like
L.prop(propName), to access a named property of an object, and
L.index(elemIndex), to access an element at a given index in an
array, and compose the path using L.compose(...lenses).
So, to just get at the titles array of the sampleTitles we can useL.prop('titles')
the lens :
`js`
L.get(L.prop('titles'), sampleTitles)
// [{ language: 'en', text: 'Title' },
// { language: 'sv', text: 'Rubrik' }]
To focus on the first element of the titles array, we compose withL.index(0)
the lens:
`js`
L.get(L.compose(L.prop('titles'), L.index(0)), sampleTitles)
// { language: 'en', text: 'Title' }
Then, to focus on the text, we compose with L.prop('text'):
`js`
L.get(L.compose(L.prop('titles'), L.index(0), L.prop('text')), sampleTitles)
// 'Title'
We can then use the same composed lens to also set the text:
`js`
L.set(
L.compose(L.prop('titles'), L.index(0), L.prop('text')),
'New title',
sampleTitles
)
// { titles: [{ language: 'en', text: 'New title' },
// { language: 'sv', text: 'Rubrik' }] }
In practise, specifying ad hoc lenses like this is not very useful. We'd like
to access a text in a given language, so we want a lens parameterized by a given
language. To create a parameterized lens, we can write a function that returns
a lens. Such a lens should then find the title in the desired
language.
Furthermore, while a simple path lens like above allows one to get and set an
existing text, it doesn't know enough about the data structure to be able to
properly insert new and remove existing texts. So, we will also need to specify
such details along with the path to focus on.
Let's then just compose a parameterized lens for accessing the
text of titles:
`js`
const textIn = language => L.compose(
L.prop('titles'),
L.normalize(R.sortBy(L.get('language'))),
L.find(R.whereEq({language})),
L.valueOr({language, text: ''}),
L.removable('text'),
L.prop('text')
)
Take a moment to read through the above definition line by line. Each part
either specifies a step in the path to select the desired element or a way in
which the data structure must be treated at that point. The
L.prop(...) parts are already familiar. The other parts we will
mention below.
#### ≡ ▶ Querying data
Thanks to the parameterized search part,
L.find(R.whereEq({language})), of the lens composition, we can use
it to query titles:
`js`
L.get(textIn('sv'), sampleTitles)
// 'Rubrik'
The L.find lens is given a predicate that it then uses to find an
element from an array to focus on. In this case the predicate is specified with
the help of Ramda's R.whereEq function
that creates an equality predicate from a given template object.
##### ≡ ▶ Missing data can be expected
Partial lenses can generally deal with missing data. In this case, when
L.find doesn't find an element, it instead works like a lens to
append a new element into an array.
So, if we use the partial lens to query a title that does not exist, we get the
default:
`js`
L.get(textIn('fi'), sampleTitles)
// ''
We get this value, rather than undefined, thanks to the L.valueOr({language,
text: ''}) part of our lens composition, which ensures that we getnull
the specified value rather than or undefined. We get the default evenundefined
if we query from :
`js`
L.get(textIn('fi'), undefined)
// ''
With partial lenses, undefined is the equivalent of.
non-existent
#### ≡ ▶ Updating data
As with ordinary lenses, we can use the same lens to update titles:
`js`
L.set(textIn('en'), 'The title', sampleTitles)
// { titles: [ { language: 'en', text: 'The title' },
// { language: 'sv', text: 'Rubrik' } ] }
#### ≡ ▶ Inserting data
The same partial lens also allows us to insert new titles:
`js`
L.set(textIn('fi'), 'Otsikko', sampleTitles)
// { titles: [ { language: 'en', text: 'Title' },
// { language: 'fi', text: 'Otsikko' },
// { language: 'sv', text: 'Rubrik' } ] }
There are a couple of things here that require attention.
The reason that the newly inserted object not only has the text property, butlanguage
also the property is due to the L.valueOr({language, text:
''}) part that we used to provide a default.
Also note the position into which the new title was inserted. The array of
titles is kept sorted thanks to the
L.normalize(R.sortBy(L.get('language'))) part of our lens.
The L.normalize lens transforms the data when either read or
written with the given function. In this case we used Ramda's
R.sortBy to specify that we want the titles
to be kept sorted by language.
#### ≡ ▶ Removing data
Finally, we can use the same partial lens to remove titles:
`js`
L.set(textIn('sv'), undefined, sampleTitles)
// { titles: [ { language: 'en', text: 'Title' } ] }
Note that a single title text is actually a part of an object. The key totext
having the whole object vanish, rather than just the property, is theL.removable('text') part of our lens composition. It makes ittext
so that when the property is set to undefined, the result will beundefined rather than merely an object without the text property.
If we remove all of the titles, we get an empty array:
`js`
L.set(L.seq(textIn('sv'), textIn('en')), undefined, sampleTitles)
// { titles: [] }
Above we use L.seq to run the L.set operation over both
of the focused titles.
Take out one (or more) L.normalize(...),
L.valueOr(...) or L.removable(...) part(s)
from the lens composition and try to predict what happens when you rerun the
examples with the modified lens composition. Verify your reasoning by actually
rerunning the examples.
For clarity, the previous code snippets avoided some of the shorthands that this
library supports. In particular,
* L.compose(...) can be abbreviated as an array
[[...]](#l-compose),L.prop(propName)
* can be abbreviated as propName, andL.set(l, undefined, s)
* can be abbreviated as L.remove(l,
s).
It is also typical to compose lenses out of short paths following the schema of
the JSON data being manipulated. Recall the lens from the start of the example:
`jsx`
L.compose(
L.prop('titles'),
L.normalize(R.sortBy(L.get('language'))),
L.find(R.whereEq({language})),
L.valueOr({language, text: ''}),
L.removable('text'),
L.prop('text')
)
Following the structure or schema of the JSON, we could break this into three
separate lenses:
* a lens for accessing the titles of a model object,
* a parameterized lens for querying a title object from titles, and
* a lens for accessing the text of a title object.
Furthermore, we could organize the lenses to reflect the structure of the JSON
model:
`js
const Title = {
text: [L.removable('text'), 'text']
}
const Titles = {
titleIn: language => [
L.find(R.whereEq({language})),
L.valueOr({language, text: ''})
]
}
const Model = {
titles: ['titles', L.normalize(R.sortBy(L.get('language')))],
textIn: language => [Model.titles, Titles.titleIn(language), Title.text]
}
`
We can now say:
`js`
L.get(Model.textIn('sv'), sampleTitles)
// 'Rubrik'
This style of organizing lenses is overkill for our toy example. In a more
realistic case the sampleTitles object would contain many more properties.Model.textIn
Also, rather than composing a lens, like above, to access a leaf
property from the root of our object, we might actually compose lenses
incrementally as we inspect the model structure.
So far we have used a lens to manipulate individual items. This library also
supports traversals that compose with lenses and can target
multiple items. Continuing on the tutorial example, let's define a traversal
that targets all the texts:
`js`
const texts = [Model.titles, L.elems, Title.text]
What makes the above a traversal is the L.elems part. The result
of composing a traversal with a lens is a traversal. The other parts of the
above composition should already be familiar from previous examples. Note how
we were able to use the previously defined Model.titles and Title.text
lenses.
Now, we can use the above traversal to collect all the texts:
`js`
L.collect(texts, sampleTitles)
// [ 'Title', 'Rubrik' ]
More generally, we can map and fold over texts. For example, we
could use L.maximumBy to find a title with the maximum length:
`js`
L.maximumBy(R.length, texts, sampleTitles)
// 'Rubrik'
Of course, we can also modify texts. For example, we could uppercase all the
titles:
`js`
L.modify(texts, R.toUpper, sampleTitles)
// { titles: [ { language: 'en', text: 'TITLE' },
// { language: 'sv', text: 'RUBRIK' } ] }
We can also manipulate texts selectively. For example, we could remove all
the texts that are longer than 5 characters:
`js`
L.remove([texts, L.when(t => t.length > 5)], sampleTitles)
// { titles: [ { language: 'en', text: 'Title' } ] }
This concludes the tutorial. The reference documentation contains lots of tiny
examples and a few more involved examples. The examples
section describes a couple of lens compositions we've found practical as well as
examples that may help to see possibilities beyond the immediately
obvious. The
wiki contains further
examples and playground links. There is also a document that describes a
simplified implementation of optics in a similar style as
the implementation of this library. Last, but perhaps not least, there is also
a page of Partial Lenses
Exercises to solve.
Optics provide a way to decouple the operation to perform on an element or
elements of a data structure from the details of selecting the element or
elements and the details of maintaining the integrity of the data structure. In
other words, a selection algorithm and data structure invariant maintenance can
be expressed as a composition of optics and used with many different operations.
Consider how one might approach the tutorial problem without
optics. One could, for example, write a collection of operations like
getText, setText, addText, and remText:
`js`
const getEntry = R.curry(
(language, data) => data.titles.find(R.whereEq({language}))
)
const hasText = R.pipe(getEntry, Boolean)
const getText = R.pipe(getEntry, R.defaultTo({}), R.prop('text'))
const mapProp = R.curry(
(fn, prop, obj) => R.assoc(prop, fn(R.prop(prop, obj)), obj)
)
const mapText = R.curry(
(language, fn, data) => mapProp(
R.map(R.ifElse(R.whereEq({language}), mapProp(fn, 'text'), R.identity)),
'titles',
data
)
)
const remText = R.curry(
(language, data) => mapProp(
R.filter(R.complement(R.whereEq({language}))),
'titles'
)
)
const addText = R.curry(
(language, text, data) => mapProp(R.append({language, text}), 'titles', data)
)
const setText = R.curry(
(language, text, data) => mapText(language, R.always(text), data)
)
You can definitely make the above operations both cleaner and more robust. For
example, consider maintaining the ordering of texts and the handling of cases
such as using addText when there already is a text in the specified languagesetText
and when there isn't. With partial optics, however, you separate the
selection and data structure invariant maintenance from the operations as
illustrated in the tutorial and due to the separation of concerns
that tends to give you a lot of robust functionality in a small amount of
code.
The combinators provided by this library
are available as named imports. Typically one just imports the library as:
`jsx`
import * as L from 'partial.lenses'
This library has historically been developed in a fairly aggressive manner so
that features have been marked as obsolete and removed in subsequent major
versions. This can be particularly burdensome for developers of libraries that
depend on partial lenses. To help the development of such libraries, this
section specifies a tiny subset of this library as stable. While it is
possible that the stable subset is later extended, nothing in the stable subset
will ever be changed in a backwards incompatible manner.
The following operations, with the below mentioned limitations, constitute the
stable subset:
* L.compose(...optics) ~> optic is stable with the exception
that one must not depend on being able to compose optics with ordinary
functions. Also, the use of arrays to denote composition is not part of the
stable subset. Note that L.compose() is guaranteed to be
equivalent to the L.identity optic.
* L.get(lens, maybeData) ~> maybeValue is stable without
limitations.
* L.lens(maybeData => maybeValue, (maybeValue, maybeData) => maybeData) ~>
lens is stable with the exception that one must not depend on the
user specified getter and setter functions being passed more than 1 and 2
arguments, respectively, and one must make no assumptions about any extra
parameters being passed.
* L.modify(optic, maybeValue => maybeValue, maybeData) ~>
maybeData is stable with the exception that one must not depend
on the user specified function being passed more than 1 argument and one must
make no assumptions about any extra parameters being passed.
* L.remove(optic, maybeData) ~> maybeData is stable without
limitations.
* L.set(optic, maybeValue, maybeData) ~> maybeData is stable without
limitations.
The main intention behind the stable subset is to enable a dependent library to
make basic use of lenses created by client code using the dependent library.
In retrospect, the stable subset has existed since version 2.2.0.
The main Partial Lenses library aims to provide robust general purpose
combinators for dealing with plain JavaScript data. Combinators that are more
experimental or specialized in purpose or would require additional dependencies
aside from the Infestines library,
which is mainly used for the currying helpers it provides, are not provided.
Currently the following additional Partial Lenses libraries exist:
* Partial Lenses History
* Partial Lenses Validation
The abstractions, traversals, lenses, and
isomorphisms, provided by this library are collectively known
as optics. Traversals can target any number of elements. Lenses are a
restriction of traversals that target a single element. Isomorphisms are a
restriction of lenses with an inverse.
In addition to basic bidirectional optics, this library also supports more
arbitrary transforms using optics with sequencing and
transform ops. Transforms allow operations, such as modifying
a part of data structure multiple times or even in a loop, that are not possible
with basic optics.
Some optics libraries provide many more abstractions, such as "optionals",
"prisms" and "folds", to name a few, forming a DAG. Aside from being
conceptually important, many of those abstractions are not only useful but
required in a statically typed setting where data structures have precise
constraints on their shapes, so to speak, and operations on data structures must
respect those constraints at all times.
On the other hand, in a dynamically typed language like JavaScript, the shapes
of run-time objects are naturally malleable. Nothing immediately breaks if a
new object is created as a copy of another object by adding or removing a
property, for example. We can exploit this to our advantage by considering all
optics as partial and manage with a smaller amount of distinct classes of
optics.
#### ≡ ▶ On partiality
By definition, a *total
function, or just a function, is defined for all possible inputs. A partial
function*, on the other hand, may not be defined for all inputs.
As an example, consider an operation to return the first element of an array.
Such an operation cannot be total unless the input is restricted to arrays that
have at least one element. One might think that the operation could be made
total by returning a special value in case the input array is empty, but that is
no longer the same operation—the special value is not the first element of
the array.
Now, in partial lenses, the idea is that in case the input does not match the
expectation of an optic, then the input is treated as being undefined, which: reading through the optic
is the equivalent of non-existent
gives undefined and writing through the optic replaces the focus with theL.prop
written value. This makes the optics in this library partial and allows
specific partial optics, such as the simple lens, to be used
in a wider range of situations than corresponding total optics.
Making all optics partial has a number of consequences. For one thing, it can
potentially hide bugs: an incorrectly specified optic treats the input as
undefined and may seem to work without raising an error. We have not found
this to be a major source of bugs in practice. However, partiality also has a
number of benefits. In particular, it allows optics to seamlessly support both
insertion and removal. It also allows to reduce the number of necessary
abstractions and it tends to make compositions of optics more concise with fewer
required parts, which both help to avoid bugs.
#### ≡ ▶ On indexing
Optics in this library support a simple unnested form of indexing. When
focusing on an array element or an object property, the index of the array
element or the key of the object property is passed as the index to user defined
functions operating on that focus.
For example:
`js`
L.get(
[L.find(R.equals('bar')), (value, index) => ({value, index})],
['foo', 'bar', 'baz']
)
// {value: 'bar', index: 1}`js`
L.modify(L.values, (value, key) => ({key, value}), {x: 1, y: 2})
// {x: {key: 'x', value: 1}, y: {key: 'y', value: 2}}
Only optics directly operating on array elements and object properties produce
indices. Most optics do not have an index of their own and they pass the index
given by the preceding optic as their index. For example, L.when
doesn't have an index by itself, but it passes through the index provided by the
preceding optic:
`js`
L.collectAs(
(value, index) => ({value, index}),
[L.elems, L.when(x => x > 2)],
[3, 1, 4, 1]
)
// [{value: 3, index: 0}, {value: 4, index: 2}]`js`
L.collectAs(
(value, key) => ({value, key}),
[L.values, L.when(x => x > 2)],
{x: 3, y: 1, z: 4, w: 1}
)
// [{value: 3, key: 'x'}, {value: 4, key: 'z'}]
When accessing a focus deep inside a data structure, the indices along the path
to the focus are not collected into a path. However, it is possible to use
index manipulating combinators to construct paths of indices and
more. For example:
`js`
L.collectAs(
(value, path) => [L.collect(L.flatten, path), value],
L.lazy(rec => L.ifElse(R.is(Object), [L.joinIx(L.children), rec], [])),
{a: {b: {c: 'abc'}}, x: [{y: [{z: 'xyz'}]}]}
)
// [ [ [ "a", "b", "c", ], "abc", ],
// [ [ "x", 0, "y", 0, "z", ], "xyz", ] ]
The reason for not collecting paths by default is that doing so would be
relatively expensive due to the additional allocations. The
L.choose combinator can also be useful in cases where there is a
need to access some index or context along the path to a focus.
#### ≡ ▶ On immutability
Starting with version 10.0.0, to strongly guide away from
mutating data structures, optics call
Object.freeze
on any new objects they create when NODE_ENV is not production.
Why only non-production builds? Because Object.freeze can be quite
expensive and the main benefit is in catching potential bugs early during
development.
Also note that optics do not implicitly "deep freeze" data structures given to
them or freeze data returned by user defined functions. Only objects newly
created by optic functions themselves are frozen.
Starting with version 13.10.0, the possibility that
optics do not unnecessarily clone input data structures is explicitly
acknowledged. In case all elements of an array or object produced by an optic
operation would be the same, as determined by
Object.is`,
then it is allowed, but not guaranteed, for the optic operation to return the
input as is.
#### ≡ ▶ On composability
A lot of libraries these days claim to be
composable. Is any collection of
functions composable? In the opinion of the author of this library, in order
for something to be called "composable", a couple of conditions must be
fulfilled:
1. There must be an operation or operations that perform composition.
2. There must be simple laws on how compositions behave.
Conversely, if there is no operation to perform composition or there are no
useful simplifying laws on how compositions behave, then one sho