Package detail

partial.lenses

calmm-js8.2kMIT14.17.0

Partial lenses is a comprehensive, high-performance optics library for JavaScript

partial, lens, isomorphism, traversal

readme

Partial Lenses · Gitter GitHub stars npm

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!

npm version Bower version Build Status Code Coverage

Contents

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.get(L.prop('titles'), sampleTitles)
// [{ language: 'en', text: 'Title' },
//  { language: 'sv', text: 'Rubrik' }]

To focus on the first element of the titles array, we compose with the L.index(0) lens:

L.get(L.compose(L.prop('titles'), L.index(0)), sampleTitles)
// { language: 'en', text: 'Title' }

Then, to focus on the text, we compose with L.prop('text'):

L.get(L.compose(L.prop('titles'), L.index(0), L.prop('text')), sampleTitles)
// 'Title'

We can then use the same composed lens to also set the text:

L.set(
  L.compose(L.prop('titles'), L.index(0), L.prop('text')),
  'New title',
  sampleTitles
)
// { titles: [{ language: 'en', text: 'New title' },
//            { language: 'sv', text: 'Rubrik' }] }

In practise, specifying ad hoc lenses like this is not very useful. We'd like to access a text in a given language, so we want a lens parameterized by a given language. To create a parameterized lens, we can write a function that returns a lens. Such a lens should then find the title in the desired language.

Furthermore, while a simple path lens like above allows one to get and set an existing text, it doesn't know enough about the data structure to be able to properly insert new and remove existing texts. So, we will also need to specify such details along with the path to focus on.

A partial lens to access title texts

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.

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.get(textIn('sv'), sampleTitles)
// 'Rubrik'

The L.find lens is given a predicate that it then uses to find an element from an array to focus on. In this case the predicate is specified with the help of Ramda's R.whereEq function that creates an equality predicate from a given template object.

Missing data can be expected

Partial lenses can generally deal with missing data. In this case, when L.find doesn't find an element, it instead works like a lens to append a new element into an array.

So, if we use the partial lens to query a title that does not exist, we get the default:

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.

Updating data

As with ordinary lenses, we can use the same lens to update titles:

L.set(textIn('en'), 'The title', sampleTitles)
// { titles: [ { language: 'en', text: 'The title' },
//             { language: 'sv', text: 'Rubrik' } ] }

Inserting data

The same partial lens also allows us to insert new titles:

L.set(textIn('fi'), 'Otsikko', sampleTitles)
// { titles: [ { language: 'en', text: 'Title' },
//             { language: 'fi', text: 'Otsikko' },
//             { language: 'sv', text: 'Rubrik' } ] }

There are a couple of things here that require attention.

The reason that the newly inserted object not only has the text property, 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.set(textIn('sv'), undefined, sampleTitles)
// { titles: [ { language: 'en', text: 'Title' } ] }

Note that a single title text is actually a part of an object. The key 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)
// { 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,

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.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)
// '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 = [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)
// [ '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.maximumBy(R.length, texts, sampleTitles)
// 'Rubrik'

Of course, we can also modify texts. For example, we could uppercase all the titles:

L.modify(texts, R.toUpper, sampleTitles)
// { titles: [ { language: 'en', text: 'TITLE' },
//             { language: 'sv', text: 'RUBRIK' } ] }

We can also manipulate texts selectively. For example, we could remove all the texts that are longer than 5 characters:

L.remove([texts, L.when(t => t.length > 5)], sampleTitles)
// { 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 = 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.

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:

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.get(
  [L.find(R.equals('bar')), (value, index) => ({value, index})],
  ['foo', 'bar', 'baz']
)
// {value: 'bar', index: 1}
L.modify(L.values, (value, key) => ({key, value}), {x: 1, y: 2})
// {x: {key: 'x', value: 1}, y: {key: 'y', value: 2}}

Only optics directly operating on array elements and object properties produce indices. Most optics do not have an index of their own and they pass the index given by the preceding optic as their index. For example, L.when doesn't have an index by itself, but it passes through the index provided by the preceding optic:

L.collectAs(
  (value, index) => ({value, index}),
  [L.elems, L.when(x => x > 2)],
  [3, 1, 4, 1]
)
// [{value: 3, index: 0}, {value: 4, index: 2}]
L.collectAs(
  (value, key) => ({value, key}),
  [L.values, L.when(x => x > 2)],
  {x: 3, y: 1, z: 4, w: 1}
)
// [{value: 3, key: 'x'}, {value: 4, key: 'z'}]

When accessing a focus deep inside a data structure, the indices along the path to the focus are not collected into a path. However, it is possible to use index manipulating combinators to construct paths of indices and more. For example:

L.collectAs(
  (value, path) => [L.collect(L.flatten, path), value],
  L.lazy(rec => L.ifElse(R.is(Object), [L.joinIx(L.children), rec], [])),
  {a: {b: {c: 'abc'}}, x: [{y: [{z: 'xyz'}]}]}
)
// [ [ [ "a", "b", "c", ], "abc", ],
//   [ [ "x", 0, "y", 0, "z", ], "xyz", ] ]

The reason for not collecting paths by default is that doing so would be relatively expensive due to the additional allocations. The L.choose combinator can also be useful in cases where there is a need to access some index or context along the path to a focus.

On immutability

Starting with version 10.0.0, to strongly guide away from mutating data structures, optics call Object.freeze on any new objects they create when NODE_ENV is not production.

Why only non-production builds? Because Object.freeze can be quite expensive and the main benefit is in catching potential bugs early during development.

Also note that optics do not implicitly "deep freeze" data structures given to them or freeze data returned by user defined functions. Only objects newly created by optic functions themselves are frozen.

Starting with version 13.10.0, the possibility that optics do not unnecessarily clone input data structures is explicitly acknowledged. In case all elements of an array or object produced by an optic operation would be the same, as determined by Object.is, then it is allowed, but not guaranteed, for the optic operation to return the input as is.

On composability

A lot of libraries these days claim to be composable. Is any collection of functions composable? In the opinion of the author of this library, in order for something to be called "composable", a couple of conditions must be fulfilled:

  1. There must be an operation or operations that perform composition.
  2. There must be simple laws on how compositions behave.

Conversely, if there is no operation to perform composition or there are no useful simplifying laws on how compositions behave, then one sho

changelog

Partial Lenses Changelog

14.14.0

Renamed L.append to L.appendTo. This means that L.append is deprecated. Code using it should be switched to use L.appendTo. This change was made due to introducing new L.prependTo and L.assignTo lenses for similar partial updates.

-L.append
+L.appendTo

Deprecated L.propsOf. L.propsOf was introduced to implement L.assign. Now L.assignTo allows for a simpler implementation of L.assign and more.

14.11.1

Fixed L.subset not to call the predicate in case the focus is already undefined.

14.2.1

Fixed L.query, L.findWith, and L.orElse (and other optics using L.orElse underneath including e.g. L.choice and L.choices) to pass the outer index to the optics passed as parameters.

14.2.0

Previously L.uriComponent only allowed strings to be encoded through it. Now it also allows booleans and numbers similarly to e.g. Node's Query String module. Because this behavior was not previously documented or tested earlier, the change is considered a bug fix.

14.1.0

Previously L.uri, L.uriComponent, and L.json threw an exception on invalid inputs. Now they instead produce the error object as their result. This behaviour was neither documented nor tested earlier, so the change is considered a bug fix.

14.0.0

The current plan is to change Partial Lenses to support so called naked or prototypeless objects with null prototype (i.e. Object.create(null)) in a following major version. This is a breaking change although it is likely that it will not affect most users. Usefully warning for this change of behaviour by adding diagnostics to optics seems somewhat difficult.

Previously obsoleted L.iftes was removed.

The L.Constant functor was removed. It can be replaced as follows:

-L.constant
+{map: (_, x) => x}

L.get has been changed to use the now exported L.Select applicative instead of the removed L.Constant functor. The reason for this change is that it both generalizes L.get and simplifies things overall.

L.get now works exactly like L.select. L.select and L.selectAs have been consequently obsoleted. Change usages as follows:

-L.select(...)
+L.get(...)
-L.selectAs(...)
+L.getAs(...)

In the cases where L.get previously returned a valid result, the only differences are in the cases where L.zero is involved. L.zero is used by several other combinators that could be used as lenses including the conditionals,

  • L.cond, and
  • L.condOf,

and the querying combinators,

  • L.chain,
  • L.choice,
  • L.optional,
  • L.unless, and
  • L.when,

and the transform ops,

  • L.assignOp,
  • L.modifyOp,
  • L.removeOp, and
  • L.setOp.

Previously L.zero was implemented so that it did something reasonable even with a plain functor. Since no operation in this library now uses a plain functor, the special case behaviour of L.zero has been removed. When previously used with L.get, L.zero passed undefined to the inner optics and ignored what the inner optics returned. For example, previously:

L.get(['x', L.when(x => x > 0), L.valueOr(0)], {x: -1})
// 0

but now L.zero exits early:

L.get(['x', L.when(x => x > 0), L.valueOr(0)], {x: -1})
// undefined

This is clearly a breaking change. However, this is unlikely to affect a large number of use cases. To get the old behavior, use of L.zero needs to be avoided. In the example case, one could write:

L.get(['x', L.ifElse(x => x > 0, [], R.always(0))], {x: -1})
// 0

13.10.0

There is no longer guarantee that optic operations return newly allocated data structures. In case all the elements of the result are the same, as determined by Object.is, as in the input, optic operations may return the input as is. OTOH, there is also currently no guarantee that input is returned as is.

13.7.4

Worked around an issue with React Native, see #161.

13.7.2

Tightened the specification of L.flatten and L.leafs to skip undefined focuses. This is considered a bug fix as the behaviour wasn't previously strictly specified.

13.6.2

Fixed a bug in L.filter, which didn't correctly handle the case of writing an empty array in case the focus wasn't an array-like object.

13.6.1

Fixed a bug in L.condOf, which didn't handle the case of zero cases correctly.

13.1.0

Obsoleted L.iftes and added L.ifElse and L.cond as the replacements for it. The motivation for the change is that formatting tools such as Prettier cannot produce readable layouts for combinators that use such non-trivial argument patterns. In cases where there is just a single predicate, use L.ifElse:

-L.iftes(predicate, consequent, alternative)
+L.ifElse(predicate, consequent, alternative)

In cases where there are multiple predicates, use L.cond:

-L.iftes(predicate1, consequent1,
-        predicate2, consequent2,
-        alternative)
+L.cond([predicate1, consequent1],
+       [predicate2, consequent2],
+       [alternative])

13.0.0

As discussed in issue #131, optics working on arrays, objects, and strings, no longer remove empty values by default. It appears that by default removal, on average, makes optics compositions more complex. In most cases this change means that uses of L.define or L.required with an empty value {}, [], or "", can simply be removed. L.define and L.required now give a warning in case they are used with an empty value and a matching empty value passes through them redundantly. In cases where removal of empty values is desired, one can e.g. compose with L.defaults. In cases where some default value is needed, one can e.g. compose with L.valueOr.

Removed previously obsoleted L.findHint.

12.0.0

As documented in 11.21.0:

  • Support for lazy algebras with the delay function was removed.
  • L.cache was removed.
  • L.augment was removed.
  • L.find and L.findWith were changed to support a hint.
  • L.findHint was marked for removal.

11.22.1

Tightened the specification of a number of isomorphisms, including L.uri, L.uriComponent, L.indexed, L.keyed, and L.reverse, so that their inverses treat unexpected inputs as undefined. This is considered a bug fix as the behaviour wasn't previously strictly specified.

11.21.0

L.cache was marked for removal. The main problem with L.cache is much like with naïve memoize implementations: the cache is stored in the wrong place, which is the point of definition of a cached (memoized) optic (function). Instead, the cache storage should be at the point of use so that when the data at the point of use is discarded so can the cache and that different points of use can each have their own cache. Otherwise it is easy to have inefficient caching and space leaks (keeping cache data around for too long).

L.augment was marked for removal. The reason for removing L.augment is that the library nowadays allows most of L.augments functionality to be implemented using simpler combinators such as L.pick with ordinary functions.

L.findHint was marked for merging into L.find. In the next major version L.find will take an optional hint parameter like current L.findHint and L.findHint will be marked for removal. Also, L.find will pass three arguments to the predicate. The third parameter is the hint object.

L.findWith was marked to be changed to support a hint parameter. This means that instead of taking multiple lenses as arguments to compose, L.findWith will, in the next major version, take a single lens and an optional hint parameter. To prepare use of L.findWith to be more compatible with the next major version, simply pass an array of the lenses:

-L.findWith(...ls)
+L.findWith([...ls])

Support for lazy algebras in the form of the delay operation was marked for removal. The reason for removing support for lazy algebras is that the next major version implements operations currently using lazy algebras, like L.select, using a different technique that is significantly faster on current JavaScript engines. That is because allocation of closures is very expensive on current JavaScript engines and lazy algebras tend to result in allocating lots of closures. Aside from performance issues, lazy algebras do, however, seem solid, but having code supporting them without actually using them internally for anything seems wasteful.

11.17.0

Fixed a bug in L.countIf. Previously it didn't pass the index to the predicate as specified in the documentation.

11.16.1

It is now guaranteed that when traversals in this library build intermediate lists of results and use of to create the initial empty list, the value used for the empty list is a unique value not otherwise exported outside of this library. In particular, the values 0, null, undefined, false, NaN, and "" are not used as an empty list. This has the benefit that it is then possible for client code to give such values special meaning in algebras.

11.0.0

Switched the order of arguments to optics so that the first two arguments are now the same as for an ordinary "read-only" function:

- (C, xi2yC, x, i) => ...
+ (x, i, C, xi2yC) => ...

This way it is not necessary to distinguish between optics and read-only functions in the get operation. On V8 based JavaScript engines this gives a significant performance improvement in some operations as taking the length of a function is very expensive in V8. This also means that the behavior of composing optics and ordinary functions is different in the sense that more arguments may be passed to an ordinary function. This change should only affect a very small number of users who have written new optics directly against the internal encoding. In such a case, you will need to switch the order of arguments as shown in the above diff. Also, if you compose optics with ordinary functions that may use more than two arguments, then you will need to limit those functions two arguments.

10.2.0

Redesigned the experimental L.findHint. The main lesson in the redesign is that the internally allocated local state for the hint was changed to be explicitly allocated by the caller. This allows the caller to update the hint and allows the search to be eliminated in more cases.

10.1.1

Previously L.append didn't provide its own index. Now it produces an index that is the length of the focused array-like object or 0. This is considered a bug fix as the behaviour wasn't previously strictly specified.

10.0.0

As discussed in issue #50, to strongly guide away from mutating data structures, optics now Object.freeze any new objects they create when NODE_ENV is not production. 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.

Removed previously obsoleted exports:

  • L.firstAs,
  • L.first,
  • L.just,
  • L.mergeAs,
  • L.merge, and
  • L.to.

See previous changelog entries on how you should deal with those.

Lazy folds are no longer considered experimental. Note, however, that the technique, an optional delay function, upon which they are based is currently not included in the Static Land specification. See issue 40 for discussion.

9.8.0

Renamed experimental L.first and L.firstAs as follows:

-L.first
+L.select

-L.firstAs
+L.selectAs

This was done to avoid confusing the operations on traversals with the newly added L.last lens on array-like objects.

9.3.0

Obsoleted L.to and L.just. You can now directly compose optics with ordinary functions (whose arity is not 4) and the result is a read-only optic. This makes L.to the same as R.identity and L.just is the same as R.always.

9.1.0

Obsoleted L.mergeAs and L.merge. L.concatAs and L.concat are now just as fast, so there is no need to have both.

9.0.0

L.augment, L.pick and L.props can now be written with an instanceof Object or undefined. Other values are considered errors. Previously they could be written with anything, but only a plain Object was considered different from undefined.

L.slice and L.filter can now be written with an array-like object or undefined. Other values are considered errors. This was the case earlier already, but now it is asserted in non-production builds.

L.index no longer produces null for previously undefined elements. L.index was changed in 4.0.0 to produce null elements. In 8.0.0 treatment of array-like objects was relaxed, but array producing optics did not consistently produce null elements. With the relaxed semantics it seems that producing null values would complicate treatment of arrays, so it seems best to just consistently produce arrays with undefined for previously undefined elements.

8.0.0

Relaxed treatment of objects and array like objects. Previously various optics required objects to have either Object or Array as the constructor. Now any instanceof Object is allowed where previously Object constructor was required and a String or an Object with non-negative integer length is allowed where previously Array constructor was required. This addresses issue 40. See the documentation of L.prop and L.index for more details. The L.branch, L.elems and L.values traversals have similarly relaxed treatment.

The previously deprecated L.sequence traversal was removed. You need to explicitly choose either L.elems or L.values.

Previously undocumented, but accidentally tested for behavior of index lenses to allow negative indices was removed. The old behavior was to ignore negative indices. The new behavior is to throw an Error in non-production builds. Behaviour in production builds is undefined.

Removed deprecated foldMapOf and collectMap. Use concatAs and collectAs instead.

7.4.0

Index lenses previously supported using negative indices so that writing through a negative index was effectively a no-op. This behavior will not be supported in the next major version.

7.3.0

Deprecated L.sequence and introduced L.elems, which operates on arrays, and, L.values, which operates on objects, to be used instead. L.sequence originally only operated on arrays, but it was generalized to operate on objects in 6.0.0. Unfortunately that turned out to be a mistake, because in the next major version, 8.0.0, the plan is to relax the treatment of objects and array like objects. The problem is that, with the generalized semantics, the type of the result, object or array, when writing through L.sequence would depend on the input in an uncontrollable manner. Apologies for the inconvenience!

7.0.0

Added minimal support for indexing. Various operations and combinators now provide an index value, either a number for an array index, or a string for an object property, or undefined in case there is no meaningful index, for the immediate index being addressed to the user-defined function taken by the operation or combinator.

6.0.0

Removed L.fromArrayBy. It was introduced as an experiment, but the use cases I had in mind didn't seem to benefit from it. If you need it, you can use this:

const fromArrayBy = id =>
  iso(xs => {
    if (R.is(Array, xs)) {
      const o = {}, n=xs.length
      for (let i=0; i<n; ++i) {
        const x = xs[i]
        o[x[id]] = x
      }
      return o
    }
  },
  o => R.is(Object, o) ? R.values(o) : undefined)

The lens L.nothing and the traversal L.skip were merged into a single L.zero optic that works like L.nothing when being viewed, using L.get, and otherwise like L.skip. The main benefit of this is that it allows "querying" combinators L.chain, L.choice, and L.when use the one and same L.zero and work without additional glue as traversals.

Generalized the L.sequence traversal to also operate on the values of objects.

Removed the defaul import. The array notation for composition is recommended as the shorthand of choice.

5.3.0

Marked the default import for removal. With the array shorthand for composition the default import is no longer worth keeping.

5.0.0

Reimplemented library internals using Static Land style dictionaries, switched to using infernals and dropped Ramda dependency and interop. These changes were made for the following reasons:

  • infernals is, and is supposed to remain, a tiny library. This is an advantage if one wishes to use lenses, but does not wish to use Ramda.

  • Performance of traversals, and folds over traversals in particular, is and can now be significantly improved, because Static Land does not require wrapping or boxing primitive values.

To interop with Ramda, you can write:

import * as L from "partial.lenses"
import * as R from "ramda"

const fromRamda = ramdaLens => L.lens(R.view(ramdaLens), R.set(ramdaLens))
const toRamda = partialLens => R.lens(L.get(partialLens), L.set(partialLens))

4.0.0

  • Removed previously deprecated functionality: removeAll.
  • Sparse arrays are no longer supported.

3.9.2

Although never explicitly specified in documentation, many of the operations and combinators were curried using Ramda's curry. Unfortunately Ramda's curry is very slow. From this version forward partial lenses no longer supports the special features of Ramda's curry like placeholders.

3.4.1

Fixed bugs when removing a non-existing property from an object or a non-existent index from an array. Previously L.remove("x", {}) returned {}. Now it returns undefined as it was previously documented. Similarly L.remove(index, []) now returns undefined as was documented.

Tightened the semantics of combinators, including L.index, L.filter, L.prop and L.augment (and other combinators whose semantics are defined in terms of those), that specifically work on objects or arrays. Previously such combinators worked asymmetrically when operating on values not in their domain. Now they consistently treat values that are not in their domain as undefined. For example, L.get("x", null) now returns undefined (previously null) and, consistently, L.set("x", 1, null) now returns {x: 1} (previously error).

3.4.0

Added minimalistic experimental traversal support in the form of the sequence traversal.

3.0.0

Dropped implicit Ramda compatibility. To interop with Ramda, one must now explicitly convert lenses using L.toRamda and L.fromRamda. In particular, L.compose no longer necessarily returns a Ramda compatible lens and, in the future, the implementation may be changed more drastically. This change was made, because now a lens returned by L.compose can take less memory and it will also be possible to further optimize the implementation in the future.

Removed deprecated functions L.view, L.over and L.firstOf.

2.2.0

Renamed L.view and L.over:

-L.view
+L.get
-L.over
+L.modify

Calling deprecated functions now results in console.warn messages.

2.1.0

Deprecated L.firstOf and added L.choice, L.nothing and L.orElse that allows the same (and more) functionality to be expressed more compositionally.

2.0.0

Changed from using a single default export to named exports to support dead-code elimination, aka tree shaking. A number of combinators were renamed in the process and the default import is now an alias for compose that may help to keep notation concise.

Upgrade guide

Now using named exports and default that aliases compose:

-import L from "partial.lenses"
+import P, * as L from "partial.lenses"

Module prefix no longer works as compose:

-L(...)
+P(...) or L.compose(...)

default is a keyword and had to be renamed:

-L.default
+L.defaults

delete is a keyword and had to be renamed:

-L.delete
+L.remove
-L.deleteAll
+L.removeAll