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!

- 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.0
L.disperse(optic, [...maybeValues], maybeData) ~> maybeData
v14.6.0
L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData
v2.2.0
L.modifyAsync(optic, (maybeValue, index) => maybeValuePromise, maybeData) ~> maybeDataPromise
v13.12.0
L.remove(optic, maybeData) ~> maybeData
v2.0.0
L.set(optic, maybeValue, maybeData) ~> maybeData
v1.0.0
L.traverse(algebra, (maybeValue, index) => operation, optic, maybeData) ~> operation
v10.0.0
- Nesting
- Recursing
- Adapting
L.choices(optic, ...optics) ~> optic
v11.10.0
L.choose((maybeValue, index) => optic) ~> optic
v1.0.0
L.cond(…[(maybeValue,
index) => testable, consequentOptic][, [alternativeOptic]]) ~>
optic v13.1.0
L.condOf(traversal,
…[(maybeValue, index) => testable, consequentOptic][,
[alternativeOptic]]) ~> optic
v13.5.0
L.ifElse((maybeValue, index) => testable, optic, optic) ~> optic
v13.1.0
L.orElse(backupOptic, primaryOptic) ~> optic
v2.1.0
- Indices
- Debugging
- Internals
- Transforms
- Traversals
- Creating new traversals
- Traversals and combinators
- Querying
- Folds over traversals
L.all((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0
L.all1((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v14.4.0
L.and(traversal, maybeData) ~> boolean
v9.6.0
L.and1(traversal, maybeData) ~> boolean
v14.4.0
L.any((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v9.6.0
L.collect(traversal, maybeData) ~> [...values]
v3.6.0
L.collectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...values]
v7.2.0
L.collectTotal(traversal, maybeData) ~> [...maybeValues]
v14.6.0
L.collectTotalAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> [...maybeValues]
v14.6.0
L.concat(monoid, traversal, maybeData) ~> value
v7.2.0
L.concatAs((maybeValue, index) => value, monoid, traversal, maybeData) ~> value
v7.2.0
L.count(traversal, maybeData) ~> number
v9.7.0
L.countIf((maybeValue, index) => testable, traversal, maybeData) ~> number
v11.2.0
L.counts(traversal, maybeData) ~> map
v11.21.0
L.countsAs((maybeValue, index) => any, traversal, maybeData) ~> map
v11.21.0
L.foldl((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0
L.foldr((value, maybeValue, index) => value, value, traversal, maybeData) ~> value
v7.2.0
L.forEach((maybeValue, index) => undefined, traversal, maybeData) ~> undefined
v11.20.0
L.forEachWith(() => context, (context, maybeValue, index) => undefined, traversal, maybeData) ~> context
v13.4.0
L.get(traversal, maybeData) ~> maybeValue
v2.2.0
L.getAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v14.0.0
L.isDefined(traversal, maybeData) ~> boolean
v11.8.0
L.isEmpty(traversal, maybeData) ~> boolean
v11.5.0
L.join(string, traversal, maybeData) ~> string
v11.2.0
L.joinAs((maybeValue, index) => maybeString, string, traversal, maybeData) ~> string
v11.2.0
L.maximum(traversal, maybeData) ~> maybeValue
v7.2.0
L.maximumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0
L.mean(traversal, maybeData) ~> number
v11.17.0
L.meanAs((maybeValue, index) => maybeNumber, traversal, maybeData) ~> number
v11.17.0
L.minimum(traversal, maybeData) ~> maybeValue
v7.2.0
L.minimumBy(keyLens, traversal, maybeData) ~> maybeValue
v11.2.0
L.none((maybeValue, index) => testable, traversal, maybeData) ~> boolean
v11.6.0
L.or(traversal, maybeData) ~> boolean
v9.6.0
L.product(traversal, maybeData) ~> number
v7.2.0
L.productAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0
L.select(traversal, maybeData) ~> maybeValue
v9.8.0
L.selectAs((maybeValue, index) => maybeValue, traversal, maybeData) ~> maybeValue
v9.8.0
L.sum(traversal, maybeData) ~> number
v7.2.0
L.sumAs((maybeValue, index) => number, traversal, maybeData) ~> number
v11.2.0
- Lenses
- Creating new lenses
L.foldTraversalLens((traversal, maybeData) => maybeValue, traversal) ~> lens
v11.5.0
L.getter((maybeData, index) => maybeValue) ~> lens
v13.16.0
L.lens((maybeData, index) => maybeValue, (maybeValue, maybeData, index) => maybeData) ~> lens
v1.0.0
L.partsOf(traversal, ...traversals) ~> lens
v14.6.0
L.setter((maybeValue, maybeData, index) => maybeData) ~> lens
v10.3.0
- Enforcing invariants
- Lensing array-like objects
- Lensing objects
- Lensing strings
- Providing defaults
- Transforming data
- Inserters
- Isomorphisms
- Interop
- Auxiliary
- Examples
- Deepening topics
- Advanced topics
- Background
- Contributing
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.
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.get(L.prop('titles'), sampleTitles)
To focus on the first element of the titles array, we compose with the
L.index(0) lens:
L.get(
L.compose(
L.prop('titles'),
L.index(0)
),
sampleTitles
)
Then, to focus on the text, we compose with L.prop('text'):
L.get(
L.compose(
L.prop('titles'),
L.index(0),
L.prop('text')
),
sampleTitles
)
We can then use the same composed lens to also set the text:
L.set(
L.compose(
L.prop('titles'),
L.index(0),
L.prop('text')
),
'New title',
sampleTitles
)
In practise, specifying ad hoc lenses like this is not very useful. We’d like to
access a text in a given language, so we want a lens parameterized by a given
language. To create a parameterized lens, we can write a function that returns a
lens. Such a lens should then find the title in the desired language.
Furthermore, while a simple path lens like above allows one to get and set an
existing text, it doesn’t know enough about the data structure to be able to
properly insert new and remove existing texts. So, we will also need to specify
such details along with the path to focus on.
Let’s then just compose a parameterized lens for accessing the
text of titles:
const textIn = language =>
L.compose(
L.prop('titles'),
L.normalize(R.sortBy(L.get('language'))),
L.find(R.whereEq({language})),
L.valueOr({language, text: ''}),
L.removable('text'),
L.prop('text')
)
Take a moment to read through the above definition line by line. Each part
either specifies a step in the path to select the desired element or a way in
which the data structure must be treated at that point. The
L.prop(...) parts are already familiar. The other parts we will
mention below.
Thanks to the parameterized search part,
L.find(R.whereEq({language})), of the lens composition, we can use
it to query titles:
L.get(textIn('sv'), sampleTitles)
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.
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.get(textIn('fi'), sampleTitles)
We get this value, rather than undefined, thanks to the
L.valueOr({language, text: ''}) part of our lens composition,
which ensures that we get the specified value rather than null or undefined.
We get the default even if we query from undefined:
L.get(textIn('fi'), undefined)
With partial lenses,
undefined is the equivalent of non-existent.
As with ordinary lenses, we can use the same lens to update titles:
L.set(textIn('en'), 'The title', sampleTitles)
The same partial lens also allows us to insert new titles:
L.set(textIn('fi'), 'Otsikko', sampleTitles)
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.
Finally, we can use the same partial lens to remove titles:
L.set(textIn('sv'), undefined, sampleTitles)
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.set(L.seq(textIn('sv'), textIn('en')), undefined, sampleTitles)
Above we use L.seq to run the L.set operation over both
of the focused titles.
Take out one (or more) L.normalize(...),
L.valueOr(...) or L.removable(...) part(s)
from the lens composition and try to predict what happens when you rerun the
examples with the modified lens composition. Verify your reasoning by actually
rerunning the examples.
For clarity, the previous code snippets avoided some of the shorthands that this
library supports. In particular,
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.compose(
L.prop('titles'),
L.normalize(R.sortBy(L.get('language'))),
L.find(R.whereEq({language})),
L.valueOr({language, text: ''}),
L.removable('text'),
L.prop('text')
)
Following the structure or schema of the JSON, we could break this into three
separate lenses:
- a lens for accessing the titles of a model object,
- a parameterized lens for querying a title object from titles, and
- a lens for accessing the text of a title object.
Furthermore, we could organize the lenses to reflect the structure of the JSON
model:
const Title = {
text: [L.removable('text'), 'text']
}
const Titles = {
titleIn: language => [
L.find(R.whereEq({language})),
L.valueOr({language, text: ''})
]
}
const Model = {
titles: ['titles', L.normalize(R.sortBy(L.get('language')))],
textIn: language => [Model.titles, Titles.titleIn(language), Title.text]
}
We can now say:
L.get(Model.textIn('sv'), sampleTitles)
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.
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 = [Model.titles, L.elems, Title.text]
What makes the above a traversal is the L.elems part. The result
of composing a traversal with a lens is a traversal. The other parts of the
above composition should already be familiar from previous examples. Note how we
were able to use the previously defined Model.titles and Title.text lenses.
Now, we can use the above traversal to collect all the texts:
L.collect(texts, sampleTitles)
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.maximumBy(R.length, texts, sampleTitles)
Of course, we can also modify texts. For example, we could uppercase all the
titles:
L.modify(texts, R.toUpper, sampleTitles)
We can also manipulate texts selectively. For example, we could remove all the
texts that are longer than 5 characters:
L.remove([texts, L.when(t => t.length > 5)], sampleTitles)
This concludes the tutorial. The reference documentation contains lots of tiny
examples and a few more involved examples. The examples
section describes a couple of lens compositions we’ve found practical as well as
examples that may help to see
possibilities beyond the immediately obvious. The
wiki contains further
examples and playground links. There is also a document that describes
a simplified implementation of optics in a similar style as
the implementation of this library. Last, but perhaps not least, there is also a
page of
Partial Lenses Exercises
to solve.
Optics provide a way to decouple the operation to perform on an element or
elements of a data structure from the details of selecting the element or
elements and the details of maintaining the integrity of the data structure. In
other words, a selection algorithm and data structure invariant maintenance can
be expressed as a composition of optics and used with many different operations.
Consider how one might approach the tutorial problem without
optics. One could, for example, write a collection of operations like getText,
setText, addText, and remText:
const getEntry = R.curry((language, data) =>
data.titles.find(R.whereEq({language}))
)
const hasText = R.pipe(
getEntry,
Boolean
)
const getText = R.pipe(
getEntry,
R.defaultTo({}),
R.prop('text')
)
const mapProp = R.curry((fn, prop, obj) =>
R.assoc(prop, fn(R.prop(prop, obj)), obj)
)
const mapText = R.curry((language, fn, data) =>
mapProp(
R.map(R.ifElse(R.whereEq({language}), mapProp(fn, 'text'), R.identity)),
'titles',
data
)
)
const remText = R.curry((language, data) =>
mapProp(R.filter(R.complement(R.whereEq({language}))), 'titles')
)
const addText = R.curry((language, text, data) =>
mapProp(R.append({language, text}), 'titles', data)
)
const setText = R.curry((language, text, data) =>
mapText(language, R.always(text), data)
)
You can definitely make the above operations both cleaner and more robust. For
example, consider maintaining the ordering of texts and the handling of cases
such as using addText when there already is a text in the specified 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.
The combinators provided by this library
are available as named imports. Typically one just imports the library as:
import * as L from 'partial.lenses'
This library has historically been developed in a fairly aggressive manner so
that features have been marked as obsolete and removed in subsequent major
versions. This can be particularly burdensome for developers of libraries that
depend on partial lenses. To help the development of such libraries, this
section specifies a tiny subset of this library as stable. While it is
possible that the stable subset is later extended, nothing in the stable subset
will ever be changed in a backwards incompatible manner.
The following operations, with the below mentioned limitations, constitute the
stable subset:
-
L.compose(...optics) ~> optic is stable with the exception
that one must not depend on being able to compose optics with ordinary
functions. Also, the use of arrays to denote composition is not part of the
stable subset. Note that L.compose() is guaranteed to be
equivalent to the L.identity optic.
-
L.get(lens, maybeData) ~> maybeValue is stable without
limitations.
-
L.lens(maybeData => maybeValue, (maybeValue, maybeData) => maybeData) ~> lens
is stable with the exception that one must not depend on the user specified
getter and setter functions being passed more than 1 and 2 arguments,
respectively, and one must make no assumptions about any extra parameters
being passed.
-
L.modify(optic, maybeValue => maybeValue, maybeData) ~> maybeData
is stable with the exception that one must not depend on the user specified
function being passed more than 1 argument and one must make no assumptions
about any extra parameters being passed.
-
L.remove(optic, maybeData) ~> maybeData is stable without
limitations.
-
L.set(optic, maybeValue, maybeData) ~> maybeData is stable without
limitations.
The main intention behind the stable subset is to enable a dependent library to
make basic use of lenses created by client code using the dependent library.
In retrospect, the stable subset has existed since version 2.2.0.
The main Partial Lenses library aims to provide robust general purpose
combinators for dealing with plain JavaScript data. Combinators that are more
experimental or specialized in purpose or would require additional dependencies
aside from the Infestines library,
which is mainly used for the currying helpers it provides, are not provided.
Currently the following additional Partial Lenses libraries exist:
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.
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.
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.get(
[L.find(R.equals('bar')), (value, index) => ({value, index})],
['foo', 'bar', 'baz']
)
L.modify(L.values, (value, key) => ({key, value}), {x: 1, y: 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.collectAs(
(value, index) => ({value, index}),
[L.elems, L.when(x => x > 2)],
[3, 1, 4, 1]
)
L.collectAs((value, key) => ({value, key}), [L.values, L.when(x => x > 2)], {
x: 3,
y: 1,
z: 4,
w: 1
})
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.collectAs(
(value, path) => [L.collect(L.flatten, path), value],
L.lazy(rec => L.ifElse(R.is(Object), [L.joinIx(L.children), rec], [])),
{a: {b: {c: 'abc'}}, x: [{y: [{z: 'xyz'}]}]}
)
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.
Starting with version 10.0.0, to strongly guide away from
mutating data structures, optics call
[Object.freeze](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Objec
README 内容过长,已截取前 64 KB。完整内容请前往 GitHub 仓库查看。
评论