≡ ▶ Partial Lenses ·
Lenses 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!
≡ ▶ Contents
- Tutorial
- The why of optics
- Reference
- Stable subset
- Additional libraries
- Optics
- On partiality
- On indexing
- On immutability
- On composability
- On lens laws
- Operations on optics
L.assign(optic, object, maybeData) ~> maybeData
v11.13.0L.disperse(optic, [...maybeValues], maybeData) ~> maybeData
v14.6.0L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData
v2.2.0L.modifyAsync(optic, (maybeValue, index) => maybeValuePromise, maybeData) ~> maybeDataPromise
v13.12.0L.remove(optic, maybeData) ~> maybeData
v2.0.0L.set(optic, maybeValue, maybeData) ~> maybeData
v1.0.0L.traverse(algebra, (maybeValue, index) => operation, optic, maybeData) ~> operation
v10.0.0
- Nesting
L.compose(...optics) ~> optic
or[...optics]
v1.0.0L.flat(...optics) ~> optic
v13.6.0
- Recursing
- Adapting
L.choices(optic, ...optics) ~> optic
v11.10.0L.choose((maybeValue, index) => optic) ~> optic
v1.0.0L.cond(...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic
v13.1.0L.condOf(traversal, ...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic
v13.5.0L.ifElse((maybeValue, index) => testable, optic, optic) ~> optic
v13.1.0L.orElse(backupOptic, primaryOptic) ~> optic
v2.1.0
- Indices
L.joinIx(optic) ~> optic
v13.15.0L.mapIx((index, maybeValue) => index) ~> optic
v13.15.0L.reIx(optic) ~> optic
v14.10.0L.setIx(index) ~> optic
v13.15.0L.skipIx(optic) ~> optic
v13.15.0L.tieIx((innerIndex, outerIndex) => index, optic) ~> optic
v13.15.0
- Debugging
L.getLog(lens, maybeData) ~> maybeValue
v13.14.0L.log(...labels) ~> optic
v3.2.0
- Internals
L.Identity ~> Monad
v13.7.0L.IdentityAsync ~> Monadish
v13.12.0L.Select ~> Applicative
v14.0.0L.toFunction(optic) ~> optic
v7.0.0
- Transforms
- Operations on transforms
- Sequencing
- Transforming
L.appendOp(value) ~> traversal
v14.14.0L.assignOp(object) ~> traversal
v11.13.0L.modifyOp((maybeValue, index) => maybeValue) ~> traversal
v11.7.0L.prependOp(value) ~> traversal
v14.14.0L.removeOp ~> traversal
v11.7.0L.setOp(maybeValue) ~> traversal
v11.7.0
- Traversals
- Creating new traversals
- Traversals and combinators
L.children ~> traversal
v13.3.0L.elems ~> traversal
v7.3.0L.elemsTotal ~> traversal
v13.11.0L.entries ~> traversal
v11.21.0L.flatten ~> traversal
v11.16.0L.keys ~> traversal
v11.21.0L.keysEverywhere ~> traversal
v14.12.0L.leafs ~> traversal
v13.3.0L.limit(count, traversal) ~> traversal
v14.10.0L.matches(/.../g) ~> traversal
v10.4.0L.offset(count, traversal) ~> traversal
v14.10.0L.query(...traversals) ~> traversal
v13.6.0L.satisfying((maybeValue, index) => testable) ~> traversal
v13.3.0L.subseq(begin, end, traversal) ~> traversal
v14.10.0L.values ~> traversal
v7.3.0L.whereEq({prop: value, ...props}) ~> traversal
v14.16.0
- Querying
- Folds over traversals
L.all((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0L.all1((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v14.4.0L.and(traversal, maybeData) ~> boolean
v9.6.0L.and1(traversal, maybeData) ~> boolean
v14.4.0L.any((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0L.collect(traversal, maybeData) ~> [...values]
v3.6.0L.collectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...values]
v7.2.0L.collectTotal(traversal, maybeData) ~> [...maybeValues]
v14.6.0L.collectTotalAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...maybeValues]
v14.6.0L.concat(monoid, traversal, maybeData) ~> value
v7.2.0L.concatAs((maybeValue, index) => value, monoid, traversal, maybeData) ~> value
v7.2.0L.count(traversal, maybeData) ~> number
v9.7.0L.countIf((maybeValue, index) => testable, traversal, maybeData) ~> number
v11.2.0L.counts(traversal, maybeData) ~> map
v11.21.0L.countsAs((maybeValue, index) => any, traversal, maybeData) ~> map
v11.21.0L.foldl((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0L.foldr((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0L.forEach((maybeValue, index) => undefined, traversal, maybeData) ~> undefined
v11.20.0L.forEachWith(() => context, (context, maybeValue, index) => undefined, traversal, maybeData) ~> context
v13.4.0L.get(traversal, maybeData) ~> maybeValue
v2.2.0L.getAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v14.0.0L.isDefined(traversal, maybeData) ~> boolean
v11.8.0L.isEmpty(traversal, maybeData) ~> boolean
v11.5.0L.join(string, traversal, maybeData) ~> string
v11.2.0L.joinAs((maybeValue, index) => maybeString, string, traversal, maybeData) ~> string
v11.2.0L.maximum(traversal, maybeData) ~> maybeValue
v7.2.0L.maximumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0L.mean(traversal, maybeData) ~> number
v11.17.0L.meanAs((maybeValue, index) => maybeNumber, traversal, maybeData) ~> number
v11.17.0L.minimum(traversal, maybeData) ~> maybeValue
v7.2.0L.minimumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0L.none((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v11.6.0L.or(traversal, maybeData) ~> boolean
v9.6.0L.product(traversal, maybeData) ~> number
v7.2.0L.productAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0L.select(traversal, maybeData) ~> maybeValue
v9.8.0L.selectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v9.8.0L.sum(traversal, maybeData) ~> number
v7.2.0L.sumAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0
- Lenses
- Creating new lenses
L.foldTraversalLens((traversal, maybeData) => maybeValue, traversal) ~> lens
v11.5.0L.getter((maybeData, index) => maybeValue) ~> lens
v13.16.0L.lens((maybeData, index) => maybeValue, (maybeValue, maybeData, index) => maybeData) ~> lens
v1.0.0L.partsOf(traversal, ...traversals) ~> lens
v14.6.0L.setter((maybeValue, maybeData, index) => maybeData) ~> lens
v10.3.0
- Enforcing invariants
- Lensing array-like objects
L.append ~> lens
v1.0.0L.cross([...lenses]) ~> lens
v14.3.0L.filter((maybeValue, index) => testable) ~> lens
v1.0.0L.find((maybeValue, index, {hint: index}) => testable[, {hint: index}]) ~> lens
v1.0.0L.findWith(optic[, {hint: index}]) ~> optic
v1.0.0L.first ~> lens
v13.1.0L.index(elemIndex) ~> lens
orelemIndex
v1.0.0L.last ~> lens
v9.8.0L.prefix(maybeEnd) ~> lens
v11.12.0L.slice(maybeBegin, maybeEnd) ~> lens
v8.1.0L.suffix(maybeBegin) ~> lens
v11.12.0
- Lensing objects
L.pickIn({prop: lens, ...props}) ~> lens
v11.11.0L.prop(propName) ~> lens
orpropName
v1.0.0L.props(...propNames) ~> lens
v1.4.0L.propsExcept(...propNames) ~> lens
v14.11.0L.propsOf(object) ~> lens
v11.13.0L.removable(...propNames) ~> lens
v9.2.0
- Lensing strings
L.matches(/.../) ~> lens
v10.4.0
- Providing defaults
L.valueOr(valueOut) ~> lens
v3.5.0
- Transforming data
- Inserters
L.appendTo ~> lens
v14.14.0L.assignTo ~> lens
v14.14.0L.prependTo ~> lens
v14.14.0
- Creating new lenses
- Isomorphisms
- Operations on isomorphisms
- Creating new isomorphisms
L.iso(maybeData => maybeValue, maybeValue => maybeData) ~> isomorphism
v5.3.0L.mapping([patternFwd, patternBwd] | (...variables) => [patternFwd, patternBwd]) ~> isomorphism
v14.8.0L._ ~> pattern
v14.8.0
L.mappings([...[patternFwd, patternBwd]] | (...variables) => [...[patternFwd, patternBwd]]) ~> isomorphism
v14.8.0L.pattern(pattern | (...variables) => pattern) ~> isomorphism
v14.13.0L.patterns([...patterns] | (...variables) => [...patterns]) ~> isomorphism
v14.13.0
- Isomorphism combinators
L.alternatives(isomorphism, ...isomorphisms) ~> isomorphism
v14.7.0L.applyAt(elementsOptic, isomorphism) ~> isomorphism
v14.9.0L.attemptEveryDown(isomorphism) ~> isomorphism
v14.13.0L.attemptEveryUp(isomorphism) ~> isomorphism
v14.13.0L.attemptSomeDown(isomorphism) ~> isomorphism
v14.13.0L.conjugate(contextIsomorphism, isomorphism) ~> isomorphism
v14.9.0L.fold(isomorphism) ~> isomorphism
v14.13.0L.inverse(isomorphism) ~> isomorphism
v4.1.0L.iterate(isomorphism) ~> isomorphism
v14.3.0L.orAlternatively(backupIsomorphism, primaryIsomorphism) ~> isomorphism
v14.7.0L.unfold(isomorphism) ~> isomorphism
v14.13.0
- Basic isomorphisms
L.complement ~> isomorphism
v9.7.0L.identity ~> isomorphism
v1.3.0L.is(value) ~> isomorphism
v11.1.0L.subset(maybeValue => testable) ~> isomorphism
v14.3.0
- Array isomorphisms
L.array(isomorphism) ~> isomorphism
v11.19.0L.arrays(isomorphism) ~> isomorphism
v14.13.0L.groupBy(keyLens) ~> isomorphism
v14.13.0L.indexed ~> isomorphism
v11.21.0L.reverse ~> isomorphism
v11.22.0L.singleton ~> isomorphism
v11.18.0L.ungroupBy(keyLens) ~> isomorphism
v14.13.0L.unzipWith1(isomorphism) ~> isomorphism
v14.13.0L.zipWith1(isomorphism) ~> isomorphism
v14.13.0
- Object isomorphisms
L.disjoint(propName => propName) ~> isomorphism
v13.13.0L.keyed ~> isomorphism
v11.21.0L.multikeyed ~> isomorphism
v14.1.0
- Standard isomorphisms
L.json({reviver, replacer, space}) ~> isomorphism
v11.3.0L.uri ~> isomorphism
v11.3.0L.uriComponent ~> isomorphism
v11.3.0
- Standardish isomorphisms
L.querystring ~> isomorphism
v14.2.0
- String isomorphisms
- Arithmetic isomorphisms
L.add(number) ~> isomorphism
v13.9.0L.divide(number) ~> isomorphism
v13.9.0L.multiply(number) ~> isomorphism
v13.9.0L.negate ~> isomorphism
v13.9.0L.subtract(number) ~> isomorphism
v13.9.0
- Interop
- Auxiliary
- Examples
- Deepening topics
- Advanced topics
- Background
- Contributing
≡ ▶ Tutorial
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, a
titles
property:
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.
≡ ▶ Getting started
Let's first import the libraries
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 use
the lens L.prop('titles')
:
L// [{ language: 'en', text: 'Title' },// { language: 'sv', text: 'Rubrik' }]
To focus on the first element of the titles
array, we compose with
the L.index(0)
lens:
L// { language: 'en', text: 'Title' }
Then, to focus on the text
, we compose with L.prop('text')
:
L// 'Title'
We can then use the same composed lens to also set the text
:
L// { 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.
≡ ▶ A partial lens to access title texts
Let's then just compose a parameterized lens for accessing the
text
of titles:
const textIn = L
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:
L// '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:
L// ''
We get this value, rather than undefined
, thanks to the L.valueOr({language, text: ''})
part of our lens composition, which ensures that we get
the specified value rather than null
or undefined
. We get the default even
if we query from undefined
:
L// ''
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:
L// { titles: [ { language: 'en', text: 'The title' },// { language: 'sv', text: 'Rubrik' } ] }
≡ ▶ Inserting data
The same partial lens also allows us to insert new titles:
L// { 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, but
also the language
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:
L// { titles: [ { language: 'en', text: 'Title' } ] }
Note that a single title text
is actually a part of an object. The key to
having the whole object vanish, rather than just the text
property, is the
L.removable('text')
part of our lens composition. It makes it
so that when the text
property is set to undefined
, the result will be
undefined
rather than merely an object without the text
property.
If we remove all of the titles, we get an empty array:
L// { titles: [] }
Above we use L.seq
to run the L.set
operation over both
of the focused titles.
≡ ▶ Exercises
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.
≡ ▶ Shorthands
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.prop(propName)
can be abbreviated aspropName
, andL.set(l, undefined, s)
can be abbreviated asL.remove(l, s)
.
≡ ▶ Systematic decomposition
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:
L
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:
const Title = text: L 'text' const Titles = L L const Model = titles: 'titles' L Modeltitles Titles Titletext
We can now say:
L// '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.
Also, rather than composing a lens, like Model.textIn
above, to access a leaf
property from the root of our object, we might actually compose lenses
incrementally as we inspect the model structure.
≡ ▶ Manipulating multiple items
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:
const texts = Modeltitles Lelems Titletext
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:
L// [ '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:
L// 'Rubrik'
Of course, we can also modify texts. For example, we could uppercase all the titles:
L// { 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:
L// { titles: [ { language: 'en', text: 'Title' } ] }
≡ ▶ Next steps
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.
≡ ▶ The why of optics
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
:
const getEntry = Rconst hasText = Rconst getText = Rconst mapProp = Rconst mapText = Rconst remText = Rconst addText = Rconst setText = R
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 language
and setText
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.
≡ ▶ Reference
The combinators provided by this library are available as named imports. Typically one just imports the library as:
import * as L from 'partial.lenses'
≡ ▶ Stable subset
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 thatL.compose()
is guaranteed to be equivalent to theL.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.
≡ ▶ Additional libraries
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:
≡ ▶ Optics
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
is the equivalent of non-existent: reading through the optic
gives undefined
and writing through the optic replaces the focus with the
written value. This makes the optics in this library partial and allows
specific partial optics, such as the simple L.prop
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:
L// {value: 'bar', index: 1}
L// {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:
L// [{value: 3, index: 0}, {value: 4, index: 2}]
L// [{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:
L// [ [ [ "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:
- There must be an operation or operations that perform composition.
- 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 should not call such a thing composable.
Now, optics are composable in several ways and in each of those ways there is an operation to perform the composition and laws on how such composed optics behave. Here is a table of the means of composition supported by this library:
Form | Operation(s) | Semantics |
---|---|---|
Nesting | L.compose(...optics) or [...optics] |
Monoid over unityped optics |
Recursing | L.lazy(optic => optic) |
Fixed point |
Adapting | L.choices(optic, ...optics) |
Semigroup over optics |
Querying | L.choice(...optics) and L.chain(value => optic, optic) |
MonadPlus over traversals |
Picking | L.pick({...prop:lens}) |
Product of lenses |
Branching | L.branch({...prop:traversal}) |
Coproduct of traversals |
Sequencing | L.seq(...transforms) |
Monad over transforms |
The above table and, in particular, the semantics column is by no means complete. In particular, the documentation of this library does not generally spell out proofs of the semantics.
≡ ▶ On lens laws
Aside from understanding laws on how forms of composition behave, it is useful to understand laws that are specific to operations on lenses and optics, in general. As described in the paper A clear picture of lens laws, many laws have been formulated for lenses and it can be useful to have lenses that do not necessarily obey some laws.
Here is a snippet that demonstrates that partial lenses can obey the laws of, so called, very well-behaved lenses:
{ return R || actual expected} const VeryWellBehavedLens = GetSet: SetGet: SetSet: // { GetSet: true, SetGet: true, SetSet: true }
You might want to ▶ play with the laws in your browser.
Note, however, that partial lenses are not (total) lenses. undefined
is
given special meaning and should not appear in the manipulated data.
≡ ▶ Myth: Partial Lenses are not lawful
For some reason there seems to be a persistent myth that partial lenses cannot obey lens laws. The issue a little more interesting than a simple yes or no. The short answer is that partial lenses can obey lens laws. However, for practical reasons there are many combinators in this library that, alone, do not obey lens laws. Nevertheless even such combinators can be used in lens compositions that obey lens laws.
Consider the L.find
combinator. The truth is that it doesn't by
itself obey lens laws. Here is an example:
L// undefined
As you can see, L.find(R.equals(1))
does not obey the SetGet
aka
Put-Get
law. Does this make the L.find
combinator useless? Far
from it.
Consider the following lens:
const valOf = L L 'val'
The valOf
lens constructor is for accessing association arrays that contain
{key, val}
pairs. For example:
const sampleAssoc = key: 'x' val: 42 key: 'y' val: 24L// [{key: 'x', val: 101}]
L// 42
L// undefined
L// [{key: 'y', val: 24}]
L// [{key: 'x', val: 13}, {key: 'y', val: 24}]
It obeys lens laws:
Before you try to break it, note that a lens returned by valOf(key)
is only
supposed to work on valid association arrays. A valid association array must
not contain duplicate keys, undefined
is not valid val
, and the order of
elements is not significant. (Note that you could also add
L.rewrite(R.sortBy(L.get('key')))
to the composition to ensure
that elements stay in the same order.)
The gist of this example is important. Even if it is the case that not all
parts of a lens composition obey lens laws, it can be that a composition taken
as a whole obeys lens laws. The reason why this use of L.find
results in a lawful partial lens is that the lenses composed after it restricts
the scope of the lens so that one cannot modify the key
.
≡ ▶ Operations on optics
≡ ▶ L.assign(optic, object, maybeData) ~> maybeData
v11.13.0
L.assign
allows one to merge the given object into the object or objects
focused on by the given optic.
For example:
L// [ { x: 3, y: 1 }, { x: 4, y: 1 } ]
≡ ▶ L.disperse(optic, [...maybeValues], maybeData) ~> maybeData
v14.6.0
L.disperse
replaces values in focuses targeted by the given optic with
optional values taken from the given array-like object.
See also L.partsOf
.
For example:
L// [[['a']], {y: 'b'}, [{l: 'c', r: ['d']}, {}]]
To understand L.disperse
, it is perhaps helpful to consider under what
conditions the following equations hold:
ColDis: L = dDisCol: L = vsDisDis: L = L
The point is that L.disperse
is roughly to L.collectTotal
as L.set
is to L.get
. However, just like with
L.set
and L.get
, the equations do not
hold for all (combinations of) optics (and arrays of values).
≡ ▶ L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData
v2.2.0
L.modify
allows one to map over the elements focused on by the given optic.
For example:
L// { elems: [ { x: 2, y: 2 }, { x: 3, y: 4 } ] }
L// { elems: [ { x: 0, y: 2 }, { x: 2, y: 4 } ] }
≡ ▶ L.modifyAsync(optic, (maybeValue, index) => maybeValuePromise, maybeData) ~> maybeDataPromise
v13.12.0
L.modifyAsync
allows one to map an asynchronous function over the elements
focused on by the given optic. The result of L.modifyAsync
is always a
promise.
For example:
// Promise { elems: [ { x: 0, y: 2 }, { x: 2, y: 4 } ] }
≡ ▶ L.remove(optic, maybeData) ~> maybeData
v2.0.0
L.remove
allows one to remove the elements focused on by the given optic.
For example:
L// [ { x: 2 }, { x: 3 } ]
L// [ { x: 1 }, { y: 1 }, {} ]
Note that L.remove(optic, maybeData)
is equivalent to L.set(lens, undefined, maybeData)
. With partial lenses, setting to undefined
typically has
the effect of removing the focused element.
≡ ▶ L.set(optic, maybeValue, maybeData) ~> maybeData
v1.0.0
L.set
allows one to replace the elements focused on by the given optic with
the specified value.
For example:
L// {a: [{x: 11}], id: 'z'}
L// [ { x: 1 }, { x: -1, y: 1 }, { x: -1 } ]
Note that L.set(lens, maybeValue, maybeData)
is equivalent to L.modify(lens, R.always(maybeValue), maybeData)
.
≡ ▶ L.traverse(algebra, (maybeValue, index) => operation, optic, maybeData) ~> operation
v10.0.0
L.traverse
maps each focus to an operation and returns an operation that runs
those operations in-order and collects the results. The
algebra
argument must be either a
Functor
,
Applicative
,
or
Monad
depending on the optic as specified in L.toFunction
.
Here is a bit involved example that uses the State applicative and L.traverse
to replace elements in a data structure by the number of times those elements
have appeared at that point in the data structure:
const State = state result { const state: state1 result: x2y = const state result: x = return state result: } State result const count = { const k = `` const n = x2nk || 0 + 1 return result: n state: L} State// [1, 1, 2, 3, 2, 1, 1, 2, 2, 1]
≡ ▶ Nesting
The L.compose
combinator allows one to build optics that deal
with nested data structures.
≡ ▶ L.compose(...optics) ~> optic
or [...optics]
v1.0.0
L.compose
creates a nested composition of the given optics and ordinary
functions such that in L.compose(bigger, smaller)
the smaller
optic can only
see and manipulate the part of the whole as seen through the bigger
optic.
See also L.toFunction
.
The following equations characterize composition:
L = Lidentity L = lL = R L = R
Furthermore, in this library, an array of optics [...optics]
is treated as a
composition L.compose(...optics)
. Using the array notation, the above
equations can be written as:
= Lidentity l = lL = R L = R
For example:
L// { a: [ 'b', 'a' ] }
L// 'c'
You can also directly compose optics with ordinary functions. The result of such a composition is a read-only optic.
For example:
L// 2
L// { x: 1 }
Note that eligible ordinary functions must have a maximum arity of two: the
first argument will be the data and second will be the index. Both can, of
course, be undefined
. Also starting from version
11.0.0 it is not guaranteed that such ordinary functions
would not be passed other arguments and therefore such functions should not
depend on the number of arguments being passed nor on any arguments beyond the
first two.
Note that R.compose
is not the same as
L.compose
as described in the implementation document.
≡ ▶ L.flat(...optics) ~> optic
v13.6.0
L.flat
is like L.compose
except that L.flatten
is composed around and between the given optics. In other words, L.flat(o1, ..., oN)
is equivalent to L.compose(L.flatten, o1, L.flatten, ..., L.flatten, oN, L.flatten)
.
≡ ▶ Recursing
The L.lazy
combinator allows one to build optics that deal with
nested or recursive data structures of arbitrary depth. It also allows one to
build transforms with loops.
≡ ▶ L.lazy(optic => optic) ~> optic
v5.1.0
L.lazy
can be used to construct optics lazily. The function given to L.lazy
is passed a forwarding proxy to its return value and can also make forward
references to other optics and possibly construct a recursive optic.
Note that when using L.lazy
to construct a recursive optic, it will only work
in a meaningful way when the recursive uses are either precomposed
or presequenced with some other optic in a way that neither causes
immediate nor unconditional recursion.
For example, here is a traversal that targets all the primitive elements in a data structure of nested arrays and objects:
const primitives = L
Note that the above creates a cyclic representation of the traversal and a
similar traversal named L.leafs
is provided out-of-the-box.
Now, for example:
L// [ 1, 2, 3, 4, 5, 6 ]
L// [ [ [ 2 ], 3 ], { y: 4 }, [ { l: 5, r: [ 6 ] }, { x: 7 } ] ]
L// [ [ [ 1 ], 2 ], {}, [ { r: [ 5 ] }, { x: 6 } ] ]
≡ ▶ Adapting
Adapting combinators allow one to build optics that adapt to their input.
≡ ▶ L.choices(optic, ...optics) ~> optic
v11.10.0
L.choices
returns a partial optic that acts like the first of the given optics
whose view is not undefined
on the given data structure. When the views of
all of the given optics are undefined
, the returned optic acts like the last
of the given optics. See also L.orElse
, L.choice
,
and L.alternatives
.
For example:
L// [ { R: 1, d: 3 }, { a: 3 }, { d: 3 } ]
≡ ▶ L.choose((maybeValue, index) => optic) ~> optic
v1.0.0
L.choose
creates an optic whose operation is determined by the given function
that maps the underlying view, which can be undefined
, to an optic. In other
words, the L.choose
combinator allows an optic to be constructed after
examining the data structure being manipulated. See also L.cond
.
For example:
const majorAxis = L L// -3
L// { x: 3, y: 1 }
≡ ▶ L.cond(...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic
v13.1.0
L.cond
creates an optic whose operation is selected from the given optics and
predicates on the underlying view. See also L.condOf
,
L.choose
and L.ifElse
.
L
L.cond
is not curried unlike most functions in this library. L.cond
can be
given any number of [predicate, consequent]
pairs. The predicates are
functions on the underlying view and are tested sequentially. The consequents
are optics and L.cond
acts like the consequent corresponding to the first
predicate that returns true. The last argument to L.cond
can be an
[alternative]
singleton, where the alternative is an optic to be used in
case none of the predicates return true. If all predicates return false and
there is no alternative, L.cond
acts like L.zero
.
For example:
const minorAxis = L L// 1
L// { x: -3, y: -1 }
Note that it is better to omit the predicate from the alternative
L
than to use a catch all predicate like R.T
L
because in the latter case L.cond
cannot determine that a user defined
predicate will always be true and has to construct a more expensive optic.
Note that when no [alternative]
is specified, L.cond
returns a
traversal, because the default L.zero
is a
traversal.
Note that L.cond
can be implemented using L.choose
, but not
vice versa. L.choose
not only allows the optic to be chosen
dynamically, but also allows the optic to be constructed dynamically and using
the data at the focus.
≡ ▶ L.condOf(traversal, ...[(maybeValue, index) => testable, consequentOptic][, [alternativeOptic]]) ~> optic
v13.5.0
L.condOf
is like L.cond
except the first argument to L.condOf
is a traversal whose focuses are tested with the predicates.
L
L.condOf
acts like the consequent optic of first [predicate, consequent]
pair whose predicate accepts any focus produced by the traversal.
The last argument to L.condOf
can be an [alternative]
singleton, where the
alternative is an optic to be used in case none of the predicates accepts
any focus produced by the traversal. If there is no [alternative]
L.zero
is used.
For example:
L// 'Try writing this with `L.cond`.'
Note that L.condOf(t, [p1, o1], ..., [pN, oN], [o])
is roughly equivalent to a
combination of L.any
and L.cond
: L.cond([L.any(p1, t), o1], ..., [L.any(pN, t), oN], [o])
.
Note that when no [alternative]
is specified, L.condOf
returns a
traversal, because the default L.zero
is a
traversal.
≡ ▶ L.ifElse((maybeValue, index) => testable, optic, optic) ~> optic
v13.1.0
L.ifElse
creates an optic whose operation is selected based on the given
predicate from the two given optics. If the predicate is truthy on the value at
focus, the first of the given optics is used. Otherwise the second of the given
optics is used. See also L.cond
.
For example:
L// [ 2, 3, 4 ]
L// { x: 2, y: 3, z: 4 }
≡ ▶ L.orElse(backupOptic, primaryOptic) ~> optic
v2.1.0
L.orElse(backupOptic, primaryOptic)
acts like primaryOptic
when its view is
not undefined
and otherwise like backupOptic
. See also
L.orAlternatively
.
Note that L.choice(...optics)
is equivalent to
optics.reduceRight(L.orElse, L.zero)
and L.choices(...optics)
is equivalent to optics.reduceRight(L.orElse)
.
≡ ▶ Indices
The indexing combinators allow one to manipulate the indices passed down by optics. Although optics do not construct paths by default one can use the indexing combinators to construct paths. Because optics do not generally depend on the index values, it is also possible to use the index to pass down arbitrary information. For example, one could collect contexts or a list of values from the path to the focus and pass that down as the index.
≡ ▶ L.joinIx(optic) ~> optic
v13.15.0
L.joinIx
pairs the index produced by the inner optic with the incoming outer
index to form a (nested) path. In case either index is undefined
, no pair is
constructed and the other index is produced as is. See also
L.skipIx
and L.mapIx
.
For example:
L// [ 'abc', [ [ 'a', 'b' ], 'c' ] ]
≡ ▶ L.mapIx((index, maybeValue) => index) ~> optic
v13.15.0
L.mapIx
passes the value returned by the given function as the index.
For example:
L// [ 'abc', [ 'a', 'b', 'c' ] ]
≡ ▶ L.reIx(optic) ~> optic
v14.10.0
L.reIx
replaces the indices of the focuses produced by the given optic with
consecutive integers starting with 0.
For example:
L// {t: 'f', i: 'n'}
≡ ▶ L.setIx(index) ~> optic
v13.15.0
L.setIx
passes the given value as the index. Note that L.setIx(v)
is
equivalent to L.mapIx(R.always(v))
. See also
L.tieIx
and List
indexing.
≡ ▶ L.skipIx(optic) ~> optic
v13.15.0
L.skipIx
passes the incoming outer index as the index from the optic. See
also L.joinIx
.
For example:
L// [ 'abc', [ 'a', 'c' ] ]
≡ ▶L.tieIx((innerIndex, outerIndex) => index, optic) ~> optic
v13.15.0
L.tieIx
sets the index to the result of the given function on the index
produced by the wrapped optic and the index passed from the outer context.
For example:
L// [ 'abc', [ 'a', 'b', 'c' ] ]
Note that both L.skipIx
and L.joinIx
can be
implemented via L.tieIx
.
≡ ▶ Debugging
≡ ▶ L.getLog(lens, maybeData) ~> maybeValue
v13.14.0
L.getLog
returns the element focused on by a lens from a data
structure like L.get
, but L.getLog
also
console.log
s
the sequence of values that the corresponding L.set
operation would
create. This can be useful for understanding why a particular value was
returned. L.getLog
, like L.log
, is intended for debugging.
For example:
L// { data: [ { x: 1 }, { y: 2 } ] } <= [ { x: 1 }, { y: 2 } ] <= { y: 2 } <= 2// 2
(If you are looking at the above snippet in the interactive version of this
page, then note that the
console.log
function is replaced by Klipse and the
replacement function unfortunately does not handle substitution strings
correctly.)
≡ ▶ L.log(...labels) ~> optic
v3.2.0
L.log(...labels)
is an identity optic that outputs
console.log
messages with the given labels (or format in
Node.js) when
data flows in either direction, get
or set
, through the lens. See also
L.getLog
.
For example:
L// x get 10// x set 11// { x: '11' }
L// get x: 10// set x: '11'// { x: '11' }
≡ ▶ Internals
≡ ▶ L.Identity ~> Monad
v13.7.0
L.Identity
is the Static
Land
compatible identity
Monad
definition used by Partial Lenses.
≡ ▶ L.IdentityAsync ~> Monadish
v13.12.0
L.IdentityAsync
is like L.Identity
, but allows values to be
thenable.
JavaScript promises do not form a
monad,
which explains the "monadish". Fortunately one usually does not want nested
promises in which case the approximation can be close enough.
≡ ▶ L.Select ~> Applicative
v14.0.0
L.Select
is the Static
Land
compatible
Applicative
definition that extends the constant functor to select the first non-undefined
element.
The basis for Select
is the following
monoid
over JavaScript values:
const Defined = undefined l !== undefined ? l : r
It is a monoid, because it satisfies the Monoid laws:
const MonoidLaws = associativity: leftIdentity: rightIdentity: // {associativity: true, leftIdentity: true, rightIdentity: true}
In Partial Lenses undefined
is used to represent
nothingness.
≡ ▶ L.toFunction(optic) ~> optic
v7.0.0
L.toFunction
converts a given optic, which can be a string, an
integer, an array, or a function to an optic function.
optic = string
| number
| [ ...optic ]
| (x, i) => /* ordinary function = read-only optic */
| (x, i, F, xi2yF) => /* optic function */
This can be useful for implementing new combinators that cannot otherwise be
implemented using the combinators provided by this library. See also
L.traverse
.
For isomorphisms and lenses, the returned optic function will have the signature
Maybe s Index Functor c Maybe a Index -> c b -> c t
for traversals the signature will be
Maybe s Index Applicative c Maybe a Index -> c b -> c t
and for transforms the signature will be
Maybe s Index Monad c Maybe a Index -> c b -> c t
Note that the above signatures are written using the "tupled" parameter notation
(...) -> ...
to denote that the functions are not curried.
The
Functor
,
Applicative
,
and
Monad
arguments are expected to conform to their Static
Land
specifications.
Note that, in conjunction with partial optics, it may be advantageous to have
the algebras to allow for partiality. With traversals it is also possible, for
example, to simply post compose optics with L.optional
to skip
undefined
elements.
Note that if you simply wish to perform an operation that needs roughly the full
expressive power of the underlying lens encoding, you should use
L.traverse
, because it is independent of the underlying
encoding, while L.toFunction
essentially exposes the underlying encoding and
it is better to avoid depending on that.
≡ ▶ Transforms
Ordinary optics are passive and bidirectional in such a way that the same optic can be both read and written through. The underlying implementation of this library also allows one to implement active operations that don't quite provide the same kind of passive bidirectionality, but can be used to flexibly modify data structures. Such operations are called transforms in this library.
Unlike ordinary optics, transforms allow for monadic sequencing, which makes it possible to operate on a part of data structure multiple times. This allows operations that are impossible to implement using ordinary optics, but also potentially makes it more difficult to reason about the results. This ability also makes it impossible to read through transforms in the same sense as with ordinary optics.
Recall that lenses have a single focus and traversals
have multiple focuses that can then be operated upon using various operations
such as L.modify
. Although it is not strictly enforced by this
library, it is perhaps clearest to think that transforms have no focuses. A
transform using transform ops, that act as traversals of no
elements, can, and perhaps preferably should, be empty and should
be executed using L.transform
, which, unlike
L.modify
, takes no user defined operation to apply to focuses.
The line between transforms and optics is not entirely clear cut in the sense that it is technically possible to use various transform ops within an ordinary optic definition. Furthermore, it is also possible to use sequencing to create transforms that have focuses that can then be operated upon. The results of such uses don't quite follow the laws of ordinary optics, but may sometimes be useful.
≡ ▶ Operations on transforms
≡ ▶ L.transform(optic, maybeData) ~> maybeData
v11.7.0
L.transform(o, s)
is shorthand for L.modify(o, x => x, s)
and
is intended for running transforms defined using transform
ops.
For example:
L// [-1, -2, -3]
Note that
L.assign(o, x, s)
is equivalent toL.transform([o, L.assignOp(x)], s)
,L.modify(o, f, s)
is equivalent toL.transform([o, L.modifyOp(f)], s)
,L.set(o, x, s)
is equivalent toL.transform([o, L.setOp(x)], s)
, andL.remove(o, s)
is equivalent toL.transform([o, L.removeOp], s)
.
≡ ▶ L.transformAsync(optic, maybeData) ~> maybeDataPromise
v13.12.0
L.transformAsync
is like L.transform
, but allows
L.modifyOp
operations to be asynchronous. The result of
L.transformAsync
is always a
promise.
For example:
// Promise { combine: 'a nested template', of: [ 'promises' ], or: 'constants' }
// Promise [-1, -2, -3]
≡ ▶ Sequencing
The L.seq
combinator allows one to build transforms
that modify their focus more than once.
≡ ▶ L.seq(...transforms) ~> transform
v9.4.0
L.seq
creates a transform that modifies the focus with each of the given
transforms in sequence.
Here is an example of a bottom-up transform over a data structure of nested objects and arrays:
const everywhere = L
The above everywhere
transform is similar to the
F.everywhere
transform
of the fastener
zipper-library. Note
that the above everywhere
and the primitives
example differ in
that primitives
only targets the non-object and non-array elements of the data
structure while everywhere
also targets those.
L// [ { xs: [ [ [ { x: [ 1 ] } ], [ { x: [ 2 ] } ] ] ] } ]
Note that L.seq
, L.choose
, and L.setOp
can be
combined together as a
Monad
= L = L
which is not the same as the querying monad.
≡ ▶ Transforming
≡ ▶ L.appendOp(value) ~> traversal
v14.14.0
L.appendOp(x)
is shorthand for [L.appendTo, L.setOp(x)]
and can be used to
append a value to an array at focus.
≡ ▶ L.assignOp(object) ~> traversal
v11.13.0
L.assignOp
creates a transform that merges the given object into the object in
focus. When used as a traversal, L.assignOp
acts as a traversal of no
elements. Usually, however, L.assignOp
is used within
transforms.
For example:
L// [ { x: 3, y: 1 }, { x: 4, y: 1 } ]
≡ ▶ L.modifyOp((maybeValue, index) => maybeValue) ~> traversal
v11.7.0
L.modifyOp
creates a transform that maps the focus with the given function.
When used as a traversal, L.modifyOp
acts as a traversal of no elements.
Usually, however, L.modifyOp
is used within transforms.
For example:
L// { xs: [ 2, 3, 4 ],// ys: [ 0, 1, 2 ] }
≡ ▶ L.prependOp(value) ~> traversal
v14.14.0
L.prependOp(x)
is shorthand for [L.prependTo, L.setOp(x)]
and can be used to
prepend a value to an array at focus.
≡ ▶ L.removeOp ~> traversal
v11.7.0
L.removeOp
is shorthand for L.setOp(undefined)
.
Here is an example based on a question from a user:
const sampleToFilter = elements: time: 1 subelements: 1 2 3 4 time: 2 subelements: 1 2 3 4 time: 3 subelements: 1 2 3 4 L// { elements: [ { time: 2, subelements: [ 3, 4 ] },// { time: 3, subelements: [ 3, 4 ] } ] }
The idea is to filter the data both by time
and by subelements
.
≡ ▶ L.setOp(maybeValue) ~> traversal
v11.7.0
L.setOp(x)
is shorthand for L.modifyOp(R.always(x))
.
≡ ▶ Traversals
A traversal operates over a collection of non-overlapping focuses that are visited only once and can, for example, be collected, folded, modified, set and removed. Put in another way, a traversal specifies a set of paths to elements in a data structure.
≡ ▶ Creating new traversals
≡ ▶ L.branch({prop: traversal, ...props}) ~> traversal
v5.1.0
L.branch
creates a new traversal from a given possibly nested template object
that specifies how the new traversal should visit the properties of an object.
If one thinks of traversals as specifying sets of paths, then the template can
be seen as mapping each property to a set of paths to traverse.
For example:
L// [ 'x', 'y' ]
The use of []
above might be puzzling at first.
[]
essentially specifies an empty path. So, when a property is
mapped to []
in the template given to L.branch
, it means that
the element is to be visited by the resulting traversal.
Note that L.branch
is equivalent to L.branchOr(L.zero)
.
Note that you can also compose L.branch
with other optics. For example, you
can compose with L.pick
to create a traversal over specific
elements of an array:
L// [ -1, 2, -3 ]
See the BST traversal section for a more meaningful example.
≡ ▶ L.branchOr(traversal, {prop: traversal, ...props}) ~> traversal
v13.2.0
L.branchOr
creates a new traversal from a given traversal and a given possibly
nested template object. The template specifies how the new traversal should
visit the corresponding properties of an object. The separate traversal is used
for properties not defined in the template.
For example:
L// { x: -1, y: 1 }
Note that L.branch
is equivalent to
L.branchOr(L.zero)
and L.values
is equivalent to
L.branchOr([], {})
.
≡ ▶ L.branches(...propNames) ~> traversal
v13.5.0
L.branches
creates a new traversal that visits the specified properties of an
object. L.branches(p1, ..., pN)
is equivalent to L.branch({[p1]: [], ..., [pN]: []})
.
≡ ▶ Traversals and combinators
≡ ▶ L.children ~> traversal
v13.3.0
L.children
is a traversal over the immediate children of the ordinary array or
plain object in focus. Children of objects whose constructor is neither Array
nor Object
are not traversed. See also L.leafs
.
For example:
L// {x: -3, y: -1}
L// [-1, -2, -3]
≡ ▶ L.elems ~> traversal
v7.3.0
L.elems
is a traversal over the elements of an array-like
object. When written through, L.elems
always produces an Array
. See also
L.values
and L.elemsTotal
.
For example:
L// { xs: [ { x: 2 }, { x: 3 } ] }
Just like with other optics operating on array-like objects, when
manipulating non-Array
objects, L.rewrite
can be used to
convert the result to the desired type, if necessary:
L// Int8Array [ 0, 5, 1, 3, 5 ]
≡ ▶ L.elemsTotal ~> traversal
v13.11.0
L.elemsTotal
is a traversal over the elements of an array-like
object. When written through, L.elemsTotal
always produces an Array
.
Unlike L.elems
, L.elemsTotal
does not remove undefined
elements from the resulting array when written through.
For example:
L// [-1, undefined, -2]
≡ ▶ L.entries ~> traversal
v11.21.0
L.entries
is a traversal over the entries, or [key, value]
pairs, of an
object.
For example:
L// { a: 'x', b: 'y' }
≡ ▶ L.flatten ~> traversal
v11.16.0
L.flatten
is a traversal over the elements of arbitrarily nested arrays.
Other array-like objects are treated as elements by L.flatten
.
In case the immediate target of L.flatten
is neither undefined
nor an array,
it is traversed.
For example:
L// '1 2 3'
≡ ▶ L.keys ~> traversal
v11.21.0
L.keys
is a traversal over the keys of an object. See also
L.keysEverywhere
.
For example:
L// { X: 1, Y: 2 }
≡ ▶ L.keysEverywhere ~> traversal
v14.12.0
L.keysEverywhere
is a traversal over the keys of objects inside arbitrarily
nested ordinary arrays and plain objects. See also L.keys
.
One use case for L.keysEverywhere
is to use it with L.applyAt
to convert keys of objects. For example:
const kebabIcamel = Lconst kebabsIcamels = L L// [{kebabCase': 'is'}, {translatedTo: 'camelCase'}]
Note that L.keysEverywhere
is roughly equivalent to:
const keysEverywhere = L
The difference is that L.keysEverywhere
does not traverse objects that have an
interesting prototype.
≡ ▶ L.leafs ~> traversal
v13.3.0
L.leafs
is a traversal that descends into ordinary arrays and plain objects
and focuses on non-undefined
elements whose constructor is neither Array
nor
Object
. See also L.children
.
For example:
L// [{x: -1, y: [-2]}, -3]
≡ ▶ L.limit(count, traversal) ~> traversal
v14.10.0
L.limit
limits the number of focuses traversed via the given traversal. See
also L.offset
and L.subseq
.
For example:
L// [-3, -1, 4]
≡ ▶ L.matches(/.../g) ~> traversal
v10.4.0
L.matches
, when given a regular expression with the
global
flag, /.../g
, is a partial traversal over the matches that the regular
expression gives over the focused string. See also L.matches
.
For example:
L// [ { name: 'first', value: 'foo' },// { name: 'second', value: 'bar' } ]
Note that an empty match terminates the traversal. It is possible to make use of that feature, but it is also possible that an empty match is due to an incorrect regular expression that can match the empty string.
≡ ▶ L.offset(count, traversal) ~> traversal
v14.10.0
L.offset
offsets skips the given number of focuses from the beginning of the
given traversal. See also L.limit
and L.subseq
.
For example:
L// [3, -1, -4]
≡ ▶ L.query(...traversals) ~> traversal
v13.6.0
L.query
is a traversal that searches for
defined elements within a nested data structure of ordinary
arrays and plain objects that are focused on by the given sequence of
traversals. L.query
gives similar power as the descendant
combinator
of CSS selectors.
Recall the tutorial example. Perhaps the easiest way to focus on all the texts is to just query for them:
L// [ 'Title', 'Rubrik' ]
So, to convert all the texts to upper case, one could write:
L// { titles: [// { language: 'en', text: 'TITLE' },// { language: 'sv', text: 'RUBRIK' } ] }
To only modify the text of a specific language, one could write:
L// { titles: [// { language: 'en', text: 'TITLE' },// { language: 'sv', text: 'Rubrik' } ] }
And one can also view the text of a specific language:
L// 'Rubrik'
Like CSS selectors, L.query
can be quite convenient, but should be used with
care. The search for matching elements can be expensive and specifying a query
that matches precisely the desired elements can be difficult.
Note that L.query(...ts)
is roughly equivalent to ts.map(t => [L.satisfying(L.isDefined(t)), t])
and
L.query(L.when(predicate))
is roughly equivalent to
L.satisfying(predicate)
.
≡ ▶ L.satisfying((maybeValue, index) => testable) ~> traversal
v13.3.0
L.satisfying
is a traversal that focuses on elements that satisfy the given
predicate within a nested data structure of ordinary arrays and plain objects.
Children of objects whose constructor is neither Array
nor Object
are not
traversed. See also L.query
and L.whereEq
.
≡ ▶ L.subseq(begin, end, traversal) ~> traversal
v14.10.0
L.subseq
only traverses the focuses between the begin
:th (inclusive) and the
end
:th (exclusive) from the given traversal. See also L.offset
and L.limit
.
For example:
L// [3, -1, 4]
Note that L.subseq
works in linear time with respect to the number of focuses
produced by the traversal given to L.subseq
.
≡ ▶ L.values ~> traversal
v7.3.0
L.values
is a traversal over the values of an instanceof Object
. When
written through, L.values
always produces an Object
. See also
L.elems
.
For example:
L// { a: -1, b: -2, c: -3 }
When manipulating objects with a non-Object
constructor
const XYZ = class { Object } { const x y z = this return x * x + y * y + z * z }
L.rewrite
can be used to convert the result to the desired type,
if necessary:
const objectTo = Object L// XYZ { x: -1, y: -2, z: -3 }
Note that L.values
is equivalent to L.branchOr([], {})
.
≡ ▶ L.whereEq({prop: value, ...props}) ~> traversal
v14.16.0
L.whereEq
looks for objects that match the given possibly nested object
template of values within an arbitrarily nested data structure of plain arrays
and objects. See also L.satisfying
.
For example:
L// { key: 2, value: 'd' }
Note that L.whereEq
can be implemented as follows:
const whereEq = L
≡ ▶ Querying
Querying combinators allow one to use optics to query data structures. Querying is distinguished from adapting in that querying defaults to an empty or read-only zero.
≡ ▶ L.chain((value, index) => optic, optic) ~> traversal
v3.1.0
L.chain
provides a monadic
chain
combinator for querying with optics. L.chain(toOptic, optic)
is equivalent to
L
Note that with the R.always
, L.chain
,
L.choice
and L.zero
combinators, one can consider
optics as subsuming the maybe monad.
≡ ▶ L.choice(...optics) ~> traversal
v2.1.0
L.choice
returns a partial optic that acts like the first of the given optics
whose view is not undefined
on the given data structure. When the views of
all of the given optics are undefined
, the returned optic acts like
L.zero
, which is the identity element of L.choice
. See also
L.choices
.
For example:
L// [ { R: 1 }, { a: 2 }, { d: 3 } ]
≡ ▶ L.optional ~> traversal
v3.7.0
L.optional
is an optic over an optional element. When used as a traversal,
and the focus is undefined
, the traversal is empty. When used as a lens, and
the focus is undefined
, the lens will be read-only.
As an example, consider the difference between:
L// [ { x: 3 }, { y: 2, x: 3 } ]
and:
L// [ { x: 3 }, { y: 2 } ]
Note that L.optional
is equivalent to L.when(x => x !== undefined)
.
≡ ▶ L.unless((maybeValue, index) => testable) ~> traversal
v12.1.0
L.unless
allows one to selectively skip elements within a traversal. See also
L.when
.
For example:
L// [ -0, -1, -2, -3, -4 ]
≡ ▶ L.when((maybeValue, index) => testable) ~> traversal
v5.2.0
L.when
allows one to selectively skip elements within a traversal. See also
L.unless
.
For example:
L// [ 0, -1, -2, -3, -4 ]
Note that L.when(p)
is equivalent to L.choose((x, i) => p(x, i) ? L.identity : L.zero)
.
≡ ▶ L.zero ~> traversal
v6.0.0
L.zero
is a traversal of no elements and is the identity element of
L.choice
and L.chain
.
For example:
L// [ 2, 3, 4 ]
≡ ▶ Folds over traversals
≡ ▶ L.all((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0
L.all
determines whether all of the elements focused on by the given traversal
satisfy the given predicate.
For example:
Lall 1 <= x && x <= 6 primitives 1 2 y: 3 l: 4 r: 5 x: 6// true
See also: L.any
, L.none
, and
L.getAs
.
≡ ▶ L.all1((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v14.4.0
L.all1
determines whether all and at least one of the elements focused on by
the given traversal satisfy the given predicate.
≡ ▶ L.and(traversal, maybeData) ~> boolean
v9.6.0
L.and
determines whether all of the elements focused on by the given traversal
are truthy.
For example:
L// true
Note that L.and
is equivalent to L.all(x => x)
. See also:
L.or
.
≡ ▶ L.and1(traversal, maybeData) ~> boolean
v14.4.0
L.and1
determines whether all and at least one of the elements focused on by
the given traversal are truthy.
≡ ▶ L.any((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0
L.any
determines whether any of the elements focused on by the given traversal
satisfy the given predicate.
For example:
L// true
See also: L.all
, L.none
, and
L.getAs
.
≡ ▶ L.collect(traversal, maybeData) ~> [...values]
v3.6.0
L.collect
returns an array of the non-undefined
elements focused on by the
given traversal or lens from a data structure. See also
L.collectTotal
.
For example:
L// [ 1, 2 ]
Note that L.collect
is equivalent to L.collectAs(x => x)
.
≡ ▶ L.collectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...values]
v7.2.0
L.collectAs
returns an array of the non-undefined
values returned by the
given function from the elements focused on by the given traversal. See also
L.collectTotalAs
.
For example:
L// [ -1, -2 ]
L.collectAs(toMaybe, traversal, maybeData)
is equivalent to
L.concatAs(toCollect, Collect, [traversal, toMaybe], maybeData)
where Collect
and toCollect
are defined as follows:
const Collect = empty: R concat: Rconcatconst toCollect = x !== undefined ? x :
So:
L// [ -1, -2 ]
The internal implementation of L.collectAs
is optimized and faster than the
above naïve implementation.
≡ ▶ L.collectTotal(traversal, maybeData) ~> [...maybeValues]
v14.6.0
L.collectTotal
returns an array of the elements focused on by the given
traversal or lens from a data structure. See also L.collect
.
L// ['a', undefined]
≡ ▶ L.collectTotalAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...maybeValues]
v14.6.0
L.collectTotalAs
returns an array of the values returned by the given function
from the elements focused on by the given traversal. See also
L.collectAs
.
≡ ▶ L.concat(monoid, traversal, maybeData) ~> value
v7.2.0
L.concat({empty, concat}, t, s)
performs a fold, using the given concat
and
empty
operations, over the elements focused on by the given traversal or lens
t
from the given data structure s
. The concat
operation and the constant
returned by empty()
should form a
monoid
over the values focused on by t
.
For example:
const Sum = 0 x + yL// 6
Note that L.concat
is staged so that after given the first argument,
L.concat(m)
, a computation step is performed.
≡ ▶ L.concatAs((maybeValue, index) => value, monoid, traversal, maybeData) ~> value
v7.2.0
L.concatAs(xMi2r, {empty, concat}, t, s)
performs a map, using given function
xMi2r
, and fold, using the given concat
and empty
operations, over the
elements focused on by the given traversal or lens t
from the given data
structure s
. The concat
operation and the constant returned by empty()
should form a
monoid
over the values returned by xMi2r
.
For example:
L// 6
Note that L.concatAs
is staged so that after given the first two arguments,
L.concatAs(f, m)
, a computation step is performed.
≡ ▶ L.count(traversal, maybeData) ~> number
v9.7.0
L.count
goes through all the elements focused on by the traversal and counts
the number of non-undefined
elements.
For example:
L// 1
≡ ▶ L.countIf((maybeValue, index) => testable, traversal, maybeData) ~> number
v11.2.0
L.countIf
goes through all the elements focused on by the traversal and counts
the number of elements for which the given predicate returns a truthy value.
For example:
L// 1
≡ ▶ L.counts(traversal, maybeData) ~> map
v11.21.0
L.counts
returns a
map
of the counts of distinct values, including undefined
, focused on by the given
traversal.
For example:
Array// [[3, 1], [1, 2], [4, 1]]
≡ ▶ L.countsAs((maybeValue, index) => any, traversal, maybeData) ~> map
v11.21.0
L.countsAs
returns a
map
of the counts of distinct values, including undefined
, returned by the given
function from the values focused on by the given traversal.
For example:
Array// [[3, 1], [1, 2], [4, 1]]
≡ ▶ L.foldl((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0
L.foldl
performs a fold from left over the elements focused on by the given
traversal. This is much like the
reduce
method of JavaScript arrays.
For example:
L// 6
Note that L.forEachWith
is much like an imperative version
of L.foldl
. Consider using it instead of using L.foldl
with an imperative
accumulator procedure.
≡ ▶ L.foldr((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0
L.foldr
performs a fold from right over the elements focused on by the given
traversal. This is much like the
reduceRight
method of JavaScript arrays.
For example:
L// 6
≡ ▶ L.forEach((maybeValue, index) => undefined, traversal, maybeData) ~> undefined
v11.20.0
L.forEach
calls the given function for each focus of the traversal.
For example:
L// 3 0// 1 0// 4 1// 1 0
≡ ▶ L.forEachWith(() => context, (context, maybeValue, index) => undefined, traversal, maybeData) ~> context
v13.4.0
L.forEachWith
first calls the given thunk to get or create a context. Then it
calls the given function, with context as the first argument, for each focus of
the traversal. Finally the context is returned. This is much like an
imperative version of L.foldl
.
For example:
L// Map { 'x' => 2, 'y' => 1 }
Note that a new Map
is returned each time the above expression is evaluated.
≡ ▶ L.get(traversal, maybeData) ~> maybeValue
v9.8.0
L.get
returns the element focused on by a lens from a data
structure or goes lazily over the elements focused on by the given
traversal and returns the first non-undefined
element. See
also L.getLog
.
For example:
L// 101
L// 2
Note that L.get
is equivalent to L.getAs(x => x)
.
≡ ▶ L.getAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v14.0.0
L.getAs
goes lazily over the elements focused on by the given traversal,
applying the given function to each element, and returns the first
non-undefined
value returned by the function.
L// -4
L.getAs
operates lazily. The user specified function is only applied to
elements until the first non-undefined
value is returned and after that
L.getAs
returns without examining more elements.
Note that L.getAs
can be used to implement many other operations over
traversals such as finding an element matching a predicate and checking whether
all/any elements match a predicate. For example, here is how you could
implement a for all predicate over traversals:
const all = !L
Now:
// true
≡ ▶ L.isDefined(traversal, maybeData) ~> boolean
v11.8.0
L.isDefined
determines whether or not the given traversal focuses on any
non-undefined
element on the given data structure. When used with a lens,
L.isDefined
basically allows you to check whether the target of the lens
exists or, in other words, whether the data structure has the targeted element.
See also L.isEmpty
.
For example:
L// false
≡ ▶ L.isEmpty(traversal, maybeData) ~> boolean
v11.5.0
L.isEmpty
determines whether or not the given traversal focuses on any
elements, undefined
or otherwise, on the given data structure. Note that when
used with a lens, L.isEmpty
always returns false
, because lenses always have
a single focus. See also L.isDefined
.
For example:
L// true
≡ ▶ L.join(string, traversal, maybeData) ~> string
v11.2.0
L.join
creates a string by joining the optional elements targeted by the given
traversal with the given delimiter.
For example:
L// '1, 3'
≡ ▶ L.joinAs((maybeValue, index) => maybeString, string, traversal, maybeData) ~> string
v11.2.0
L.joinAs
creates a string by converting the elements targeted by the given
traversal to optional strings with the given function and then joining those
strings with the given delimiter.
For example:
L// '{'x':1}, {'y':2}'
≡ ▶ L.maximum(traversal, maybeData) ~> maybeValue
v7.2.0
L.maximum
computes a maximum of the optional elements targeted by the
traversal.
For example:
L// 3
Note that elements are ordered according to the >
operator.
≡ ▶ L.maximumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0
L.maximumBy
computes a maximum of the elements targeted by the traversal based
on the optional keys returned by the given lens or function. Elements for which
the returned key is undefined
are skipped.
For example:
L// 'second'
Note that keys are ordered according to the >
operator.
≡ ▶ L.mean(traversal, maybeData) ~> number
v11.17.0
L.mean
computes the arithmetic mean of the optional numbers targeted by the
traversal.
For example:
L// 1.5
≡ ▶ L.meanAs((maybeValue, index) => maybeNumber, traversal, maybeData) ~> number
v11.17.0
L.meanAs
computes the arithmetic mean of the optional numbers returned by the
given function for the elements targeted by the traversal.
For example:
L// 3.5
≡ ▶ L.minimum(traversal, maybeData) ~> maybeValue
v7.2.0
L.minimum
computes a minimum of the optional elements targeted by the
traversal.
For example:
L// 1
Note that elements are ordered according to the <
operator.
≡ ▶ L.minimumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0
L.minimumBy
computes a minimum of the elements targeted by the traversal based
on the optional keys returned by the given lens or function. Elements for which
the returned key is undefined
are skipped.
For example:
L// {x: -3}
Note that keys are ordered according to the <
operator.
≡ ▶ L.none((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v11.6.0
L.none
determines whether none of the elements focused on by the given
traversal satisfy the given predicate.
For example:
L// false
See also: L.all
, L.any
, and L.getAs
.
≡ ▶ L.or(traversal, maybeData) ~> boolean
v9.6.0
L.or
determines whether any of the elements focused on by the given traversal
is truthy.
For example:
L// false
Note that L.or
is equivalent to L.any(x => x)
. See also:
L.and
.
≡ ▶ L.product(traversal, maybeData) ~> number
v7.2.0
L.product
computes the product of the optional numbers targeted by the
traversal.
For example:
LproductLelems 1 2 3// 6
≡ ▶ L.productAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0
L.productAs
computes the product of the numbers returned by the given function
for the elements targeted by the traversal.
For example:
L// 27
Note that unlike many other folds, L.productAs
expects the function to only
return numbers and undefined
is not treated in a special way. If you need to
skip elements, you can return the number 1
.
≡ ▶ L.select(traversal, maybeData) ~> maybeValue
v9.8.0
L.select(traversal, maybeData) ~> maybeValue
v9.8.0WARNING: L.select
has been obsoleted. Just use L.get
. See
CHANGELOG for details.
L.select
goes lazily over the elements focused on by the given traversal and
returns the first non-undefined
element.
L// 2
Note that L.select
is equivalent to L.selectAs(x => x)
.
≡ ▶ L.selectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v9.8.0
L.selectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v9.8.0WARNING: L.selectAs
has been obsoleted. Just use L.getAs
.
See CHANGELOG for details.
L.selectAs
goes lazily over the elements focused on by the given traversal,
applying the given function to each element, and returns the first
non-undefined
value returned by the function.
L// -4
L.selectAs
operates lazily. The user specified function is only applied to
elements until the first non-undefined
value is returned and after that
L.selectAs
returns without examining more elements.
Note that L.selectAs
can be used to implement many other operations over
traversals such as finding an element matching a predicate and checking whether
all/any elements match a predicate. For example, here is how you could
implement a for all predicate over traversals:
const all = !L
Now:
// true
≡ ▶ L.sum(traversal, maybeData) ~> number
v7.2.0
L.sum
computes the sum of the optional numbers targeted by the traversal.
For example:
L// 6
≡ ▶ L.sumAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0
L.sumAs
computes the sum of the numbers returned by the given function for the
elements targeted by the traversal.
For example:
L// 9
Note that unlike many other folds, L.sumAs
expects the function to only return
numbers and undefined
is not treated in a special way. If you need to skip
elements, you can return the number 0
.
≡ ▶ Lenses
Lenses always have a single focus which can be viewed directly. Put in another way, a lens specifies a path to a single element in a data structure.
≡ ▶ Creating new lenses
≡ ▶ L.foldTraversalLens((traversal, maybeData) => maybeValue, traversal) ~> lens
v11.5.0
L.foldTraversalLens
creates a lens from a fold and a traversal. To make
sense, the fold should compute or pick a representative from the elements
focused on by the traversal such that when all the elements are equal then so is
the representative. See also L.partsOf
.
For example:
L// 1
L// [ 2, 2, 2 ]
See the Collection toggle section for a more interesting example.
≡ ▶ L.getter((maybeData, index) => maybeValue) ~> lens
v13.16.0
L.getter(get)
is shorthand for L.lens(get, x => x)
. See also
L.reread
.
≡ ▶ L.lens((maybeData, index) => maybeValue, (maybeValue, maybeData, index) => maybeData) ~> lens
v1.0.0
L.lens
creates a new primitive lens. The first parameter is the getter and
the second parameter is the setter. The setter takes two parameters: the
first is the value written and the second is the data structure to write into.
One should think twice before introducing a new primitive lens—most of the
combinators in this library have been introduced to reduce the need to write new
primitive lenses. With that said, there are still valid reasons to create new
primitive lenses. For example, here is a lens that we've used in production,
written with the help of Moment.js, to bidirectionally
convert a pair of start
and end
times to a duration:
const timesAsDuration = L
Now, for example:
L// 'PT10H'
L// { end: '2016-12-07T19:39:02.451Z',// start: '2016-12-07T09:39:02.451Z' }
When composed with L.pick
, to flexibly pick the start
and end
times, the above can be adapted to work in a wide variety of cases. However,
the above lens will never be added to this library, because it would require
adding dependency to Moment.js.
See the Interfacing with Immutable.js section for another
example of using L.lens
.
≡ ▶ L.partsOf(traversal, ...traversals) ~> lens
v14.6.0
L.partsOf
creates a lens from a given traversal composed from
the arguments. When read through, the result is always an array of elements
targeted by the traversal as if produced by L.collectTotal
.
When written through, the elements of the written
array-like object are used to replace the focuses of the
traversal as if done by L.disperse
. See also
L.foldTraversalLens
.
For example:
L// [{x: 3}, {y: 2, x: 4}]
≡ ▶ L.setter((maybeValue, maybeData, index) => maybeData) ~> lens
v10.3.0
L.setter(set)
is shorthand for L.lens(x => x, set)
. See also
L.rewrite
.
≡ ▶ Enforcing invariants
≡ ▶ L.defaults(valueIn) ~> lens
v2.0.0
L.defaults
is used to specify a default context or value for an element in
case it is missing. When set with the default value, the effect is to remove
the element. This can be useful for both making partial lenses with propagating
removal and for avoiding having to check for and provide default values
elsewhere. See also L.valueOr
.
For example:
L// []
L// [ 1, 2, 3 ]
L// {}
Note that L.defaults(valueIn)
is equivalent to L.replace(undefined, valueIn)
.
≡ ▶ L.define(value) ~> lens
v1.0.0
L.define
is used to specify a value to act as both the default value and the
required value for an element.
L// null
L// { y: 10, x: null }
Note that L.define(value)
is equivalent to [L.required(value), L.defaults(value)]
.
≡ ▶ L.normalize((value, index) => maybeValue) ~> lens
v1.0.0
L.normalize
maps the value with same given transform when read and written and
implicitly maps undefined
to undefined
. L.normalize(fn)
is equivalent to
composing L.reread(fn)
and L.rewrite(fn)
.
One use case for normalize
is to make it easy to determine whether, after a
change, the data has actually changed. By keeping the data normalized, a simple
R.equals
comparison will do.
≡ ▶ L.required(valueOut) ~> lens
v1.0.0
L.required
is used to specify that an element is not to be removed; in case it
is removed, the given value will be substituted instead.
For example:
L// {}
L// { item: null }
Note that L.required(valueOut)
is equivalent to L.replace(valueOut, undefined)
.
≡ ▶ L.reread((valueIn, index) => maybeValueIn) ~> lens
v11.21.0
L.reread
maps the value with the given transform on read and implicitly maps
undefined
to undefined
. See also L.normalize
and
L.getter
.
≡ ▶ L.rewrite((valueOut, index) => maybeValueOut) ~> lens
v5.1.0
L.rewrite
maps the value with the given transform when written and implicitly
maps undefined
to undefined
. See also L.normalize
and
L.setter
.
One use case for rewrite
is to re-establish data structure invariants after
changes.
See the BST as a lens section for a meaningful example.
≡ ▶ Lensing array-like objects
Objects that have a non-negative integer length
and strings, which are not
considered Object
instances in JavaScript, are considered array-like objects
by partial optics. See also L.seemsArrayLike
.
When writing through a lens or traversal that operates on array-like objects,
the result is always a plain Array
. For example:
L// [ 'L', 'a', 'L', 'a' ]
It may seem like the result should be of the same type as the object being manipulated, but that is problematic, because
- the focus of a partial optic is always optional, so there might not be an original array-like object whose type to use, and
- manipulation of the elements can change their types, so they may no longer be compatible with the type of the original array-like object.
Therefore, instead, when manipulating strings or array-like non-Array
objects,
L.rewrite
can be used to explicitly convert the result to the
desired type, if necessary. For example:
L// 'LaLa'
Also, when manipulating array-like objects, partial lenses generally ignore
everything but the length
property and the integer properties from 0
to
length-1
.
≡ ▶ L.append ~> lens
v1.0.0
WARNING: L.append
has been renamed to L.appendTo
. See
CHANGELOG for details.
≡ ▶ L.cross([...lenses]) ~> lens
v14.3.0
L.cross
constructs a lens or isomorphism between fixed length arrays or tuples
from the given array of lenses or isomorphisms. The optic returned by L.cross
is strict such that in case any elements of the resulting array in either
direction would be undefined
then the whole result will be undefined
.
For example
L// [ 1, 2, 4 ]
L// [ { x: -1, y: 2 }, -2, { x: 3, y: -4 } ]
≡ ▶ L.filter((maybeValue, index) => testable) ~> lens
v1.0.0
L.filter
operates on array-like objects. When not viewing an
array-like object, the result is undefined
. When viewing an array-like
object, only elements matching the given predicate will be returned. When set,
the resulting array will be formed by concatenating the elements of the set
array-like object and the elements of the complement of the filtered focus.
For example:
L// [ 'a', 'b', 'c', 'd', '3', '4', '5', '9' ]
NOTE: If you are merely modifying a data structure, and don't need to limit
yourself to lenses, consider using the L.elems
traversal composed
with L.when
.
An alternative design for filter could implement a smarter algorithm to combine
arrays when set. For example, an algorithm based on edit
distance could be used to maintain
relative order of elements. While this would not be difficult to implement, it
doesn't seem to make sense, because in most cases use of
L.normalize
or L.rewrite
would be preferable.
Also, the L.elems
traversal composed with L.when
will
retain order of elements.
≡ ▶ L.find((maybeValue, index, {hint: index}) => testable[, {hint: index}]) ~> lens
v1.0.0
L.find
operates on array-like objects like
L.index
, but the index to be viewed is determined by finding the
first element from the focus that matches the given predicate. When no matching
element is found the effect is same as with L.appendTo
.
L// [ 3, 4, 1, 5, 9, 2 ]
L.find
is designed to operate efficiently when used repeatedly. To this end,
L.find
can be given an object with a hint
property and when no hint object
is passed, a new object will be allocated internally. Repeated searches are
started from the closest existing index to the hint
and then by increasing
distance from that index. The hint
is updated after each search and the
hint
can also be mutated from the outside. The hint
object is also passed
to the predicate as the third argument. This makes it possible to both
practically eliminate the linear search and to implement the predicate without
allocating extra memory for it.
For example:
L// [{id: 3, value: 'a'},// {id: 2, value: 'B'},// {id: 1, value: 'c'},// {id: 4, value: 'd'},// {id: 5, value: 'e'}]
Note that L.find
by itself does not satisfy all lens laws. To fix this, you
can e.g. post compose L.find
with lenses that ensure that the property being
tested by the predicate given to L.find
cannot be written to. See
here for discussion and an example.
≡ ▶ L.findWith(optic[, {hint: index}]) ~> optic
v1.0.0
L.findWith
chooses an index from an array-like object through
which the given optic has a non-undefined
view and then returns an optic that
focuses on that.
For example:
L// 9
L// [ { z: 6 }, { x: 3 }, { y: 6 } ]
≡ ▶ L.first ~> lens
v13.1.0
L.first
is a synonym for L.index(0)
or 0
and
focuses on the first element of an array-like object or works
like L.appendTo
in case no such element exists. See also
L.last
.
For example:
L// 'a'
≡ ▶ L.index(elemIndex) ~> lens
or elemIndex
v1.0.0
L.index(elemIndex)
or just elemIndex
focuses on the element at specified
index of an array-like object.
- When not viewing an index with a defined element, the result is
undefined
. - When setting to
undefined
, the element is removed from the resulting array, shifting all higher indices down by one. - When setting a defined value to an index that is higher than the length of the
array-like object, the missing elements will be filled with
undefined
.
For example:
L// [ 'x', 'y', 'z' ]
L// [ ]
≡ ▶ L.last ~> lens
v9.8.0
L.last
focuses on the last element of an array-like object or
works like L.appendTo
in case no such element exists. See also
L.first
.
Focusing on an empty array or undefined
results in returning undefined
. For
example:
L// 3
L// undefined
Setting value with L.last
sets the last element of the object or appends the
value if the focused object is empty or undefined
. For example:
L// [1, 2, 5]
L// [1]
≡ ▶ L.prefix(maybeEnd) ~> lens
v11.12.0
L.prefix
focuses on a range of elements of an array-like object
starting from the beginning of the object. L.prefix
is a special case of
L.slice
.
The end of the range is determined as follows:
- non-negative values are relative to the beginning of the array-like object,
Infinity
is the end of the array-like object,- negative values are relative to the end of the array-like object,
-Infinity
is the beginning of the array-like object, andundefined
is the end of the array-like object.
For example:
L// [ 1, 2, 3 ]
≡ ▶ L.slice(maybeBegin, maybeEnd) ~> lens
v8.1.0
L.slice
focuses on a specified range of elements of an
array-like object. See also L.prefix
and
L.suffix
.
The range is determined like with the standard
slice
method of arrays:
- non-negative values are relative to the beginning of the array-like object,
Infinity
is the end of the array-like object,- negative values are relative to the end of the array-like object,
-Infinity
is the beginning of the array-like object, andundefined
gives the defaults: 0 for the begin and length for the end.
For example:
L// [ 2, 3 ]
L// [ 1, 2, 0 ]
≡ ▶ L.suffix(maybeEnd) ~> lens
v11.12.0
L.suffix
focuses on a range of elements of an array-like object
starting from the end of the object. L.suffix
is a special case of
L.slice
.
The beginning of the range is determined as follows:
- non-negative values are relative to the end of the array-like object,
Infinity
is the beginning of the array-like object,- negative values are relative to the beginning of the array-like object,
-Infinity
is the end of the array-like object, andundefined
is the beginning of the array-like object.
Note that the rules above are different from the rules for determining the
beginning of L.slice
.
For example:
L// [ 3, 1, 4, 1 ]
≡ ▶ Lensing objects
Anything that is an instanceof Object
is considered an object by partial
lenses.
When writing through an optic that operates on objects, the result is always a
plain Object
. For example:
{ thisgold = gold thissilver = silver thisbronze = bronze} L// { gold: 1, silver: -2, bronze: 3 }
When manipulating objects whose constructor is not Object
,
L.rewrite
can be used to convert the result to the desired type,
if necessary:
L// Custom { gold: 1, silver: -2, bronze: 3 }
Partial lenses also generally guarantees that the creation order of keys is preserved (even though the library used to print out evaluation results from code snippets might not preserve the creation order). For example:
for const k in L console// gold// silver// bronze
When creating new objects, partial lenses generally ignore everything but own string keys. In particular, properties from the prototype chain are not copied and neither are properties with symbol keys.
≡ ▶ L.pickIn({prop: lens, ...props}) ~> lens
v11.11.0
L.pickIn
creates a lens from the given possibly nested object template of
lenses similar to L.pick
except that the lenses in the template are
relative to their path in the template. This means that using L.pickIn
you
can effectively create a kind of filter for a nested object structure. See also
L.props
.
For example:
L// { meta: { file: './foo.txt', ext: 'txt' } }
≡ ▶ L.prop(propName) ~> lens
or propName
v1.0.0
L.prop(propName)
or just propName
focuses on the specified object property.
- When not viewing a defined object property, the result is
undefined
. - When writing to a property, the result is always an
Object
. - When setting property to
undefined
, the property is removed from the result.
When setting or removing properties, the order of keys is preserved.
For example:
L// 2
L// { x: 1, y: -2, z: 3 }
When manipulating objects whose constructor is not Object
,
L.rewrite
can be used to convert the result to the desired type,
if necessary:
L// XYZ { x: 3, y: 1, z: 3 }
≡ ▶ L.props(...propNames) ~> lens
v1.4.0
L.props
focuses on a subset of properties of an object, allowing one to treat
the subset of properties as a unit. The view of L.props
is undefined
when
none of the properties is defined. This allows L.props
to be used with
e.g. L.choices
. Otherwise the view is an object containing a
subset of the properties. Setting through L.props
updates the whole subset of
properties, which means that any missing properties are removed if they did
exists previously. When set, any extra properties are ignored. See also
L.propsExcept
.
L// { x: 4, z: 3 }
Note that L.props(k1, ..., kN)
is equivalent to L.pick({[k1]: k1, ..., [kN]: kN})
and L.pickIn({[k1]: [], ..., [kN]: []})
.
≡ ▶ L.propsExcept(...propNames) ~> lens
v14.11.0
L.propsExcept
focuses on all the properties of an object except the specified
properties. See also L.props
.
L// [{id: 1, x: 3, z: 4}, {id: 2, x: 2}, {id: 3, x: 1, y: 2}]
≡ ▶ L.propsOf(object) ~> lens
v11.13.0
L.propsOf(object) ~> lens
v11.13.0WARNING: propsOf
has been deprecated and there is no replacement. See
CHANGELOG for details.
L.propsOf(o)
is shorthand for L.props(...Object.keys(o))
allowing one to focus on the properties specified via the given sample object.
≡ ▶ L.removable(...propNames) ~> lens
v9.2.0
L.removable
creates a lens that, when written through, replaces the whole
result with undefined
if none of the given properties is defined in the
written object. L.removable
is designed for making removal propagate through
objects.
Contrast the following examples:
L// { y: 2 }
L// undefined
Also note that, in a composition, L.removable
is likely preceded by
L.valueOr
(or L.defaults
) like in the
tutorial example. In such a pair, the preceding lens gives a
default value when reading through the lens, allowing one to use such a lens to
insert new objects. The following lens then specifies that removing the then
focused property (or properties) should remove the whole object. In cases where
the shape of the incoming object is know, L.defaults
can
replace such a pair.
≡ ▶ Lensing strings
≡ ▶ L.matches(/.../) ~> lens
v10.4.0
L.matches
, when given a regular expression without the
global
flags, /.../
, is a partial lens over the match. When there is no match, or
the target is not a string, then L.matches
will be read-only. See also
L.matches
.
For example:
L// '/dir/file.txt'
≡ ▶ Providing defaults
≡ ▶ L.valueOr(valueOut) ~> lens
v3.5.0
L.valueOr
is an asymmetric lens used to specify a default value in case the
focus is undefined
or null
. When set, L.valueOr
behaves like the identity
lens. See also L.defaults
.
For example:
L// 0
L// 0
L// undefined
Note that L.valueOr(otherwise)
is equivalent to L.getter(x => x != null ? x : otherwise)
.
≡ ▶ Transforming data
≡ ▶ L.pick({prop: lens, ...props}) ~> lens
v1.2.0
L.pick
creates a lens out of the given possibly nested object template of
lenses and allows one to pick apart a data structure and then put it back
together. When viewed, undefined
properties are not added to the result and
if the result would be an empty object, the result will be undefined
. This
allows L.pick
to be used with e.g. L.choices
. Otherwise an
object is created, whose properties are obtained by viewing through the lenses
of the template. When set with an object, the properties of the object are set
to the context via the lenses of the template.
For example, let's say we need to deal with data and schema in need of some semantic restructuring:
const sampleFlat = px: 1 py: 2 vx: 1 vy: 0
We can use L.pick
to create a lens to pick apart the data and put it back
together into a more meaningful structure:
const sanitize = L
Note that in the template object the lenses are relative to the root focus of
L.pick
.
We now have a better structured view of the data:
L// { pos: { x: 1, y: 2 }, vel: { x: 1, y: 0 } }
That works in both directions:
L// { px: 6, py: 2, vx: 1, vy: 0 }
NOTE: In order for a lens created with L.pick
to work in a predictable
manner, the given lenses must operate on independent parts of the data
structure. As a trivial example, in L.pick({x: 'same', y: 'same'})
both of
the resulting object properties, x
and y
, address the same property of the
underlying object, so writing through the lens will give unpredictable results.
Note that, when set, L.pick
simply ignores any properties that the given
template doesn't mention. Also note that the underlying data structure need not
be an object.
Note that the sanitize
lens defined above can also been seen as an
isomorphism between the "flat" and "nested" forms of the data.
It can even be inverted using L.inverse
:
L// { px: 1, py: 2, vx: 1, vy: 0 }
≡ ▶ L.replace(maybeValueIn, maybeValueOut) ~> lens
v1.0.0
L.replace(maybeValueIn, maybeValueOut)
, when viewed, replaces the value
maybeValueIn
with maybeValueOut
and vice versa when set.
For example:
L// 2
L// 1
The main use case for replace
is to handle optional and required properties
and elements. In most cases, rather than using replace
, you will make
selective use of defaults
, required
and
define
.
≡ ▶ Inserters
The term "inserter" here is used to refer to write-only lenses that focus on a location where a new value can be inserted. Aside from the inserters listed in this section, other inserters can be obtained as special cases of other optics.
Here are a few examples of inserters obtained as special cases:
L// 'prefix'
L// 'suffix'
L// [ 3, 1, 4, 1 ]
≡ ▶ L.appendTo ~> lens
v14.14.0
L.appendTo
is a write-only lens that can be used to append values to an
array-like object. The view of L.appendTo
is always
undefined
. See also L.prependTo
and
L.assignTo
.
For example:
L// undefined
L// [ 'x' ]
L// [ 'z', 'y', 'x' ]
Note that L.appendTo
is equivalent to L.index(i)
with the index
i
set to the length of the focused array or 0 in case the focus is not a
defined array.
≡ ▶ L.assignTo ~> lens
v14.14.0
L.assignTo
is a write-only lens that can be used to assign properties to an
object. The view of L.assignTo
is always undefined
. See also
L.appendTo
and L.prependTo
.
For example:
L// { x: 3, y: 1, z: 4 }
One use case for L.assignTo
is when assigning properties to multiple focuses
through L.disperse
or L.partsOf
:
L// [ { x: 3, y: 1}, { x: 4, y: 1 } ]
≡ ▶ L.prependTo ~> lens
v14.14.0
L.prependTo
is a write-only lens that can be used to prepend values to an
array-like object. The view of L.prependTo
is always
undefined
. See also L.appendTo
and
L.assignTo
.
For example:
L