diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md new file mode 100644 index 00000000..bafd486f --- /dev/null +++ b/IMPLEMENTATION.md @@ -0,0 +1,284 @@ +# [▶](https://calmm-js.github.io/partial.lenses/implementation.html#) Partial Lenses Implementation · [![Gitter](https://img.shields.io/gitter/room/calmm-js/chat.js.svg)](https://gitter.im/calmm-js/chat) [![GitHub stars](https://img.shields.io/github/stars/calmm-js/partial.lenses.svg?style=social)](https://github.com/calmm-js/partial.lenses) [![npm](https://img.shields.io/npm/dm/partial.lenses.svg)](https://www.npmjs.com/package/partial.lenses) + +This document describes a simplified implementation of lenses and traversals +using a similar approach as Partial Lenses. The implementation of Partial +Lenses is far from simplified. It lifts strings, numbers, and arrays to optics +for notational convenience, it has been manually tweaked for size, optimized for +performance, and it also tries to handle a lot of corner cases induced by +JavaScript. All of this makes the implementation difficult to understand on its +own. The intention behind this document is to describe a simple implementation +based on which it should be easier to look at the Partial Lenses source code and +understand what is going on. + +There are many approaches to optics. Partial Lenses is based on the ideas +described by Twan van Laarhoven in [CPS based functional +references](https://www.twanvl.nl/blog/haskell/cps-functional-references) and +further by Russell O'Connor in [Polymorphic Update with van Laarhoven +Lenses](http://r6.ca/blog/20120623T104901Z.html). + +One way to think of lenses and traversals is as being an application of a single +generalized `traverse` function. The +[`traverse`](http://hackage.haskell.org/package/base-4.10.1.0/docs/Data-Traversable.html#v:traverse) +function of the `Traversable` constructor class + +```haskell +traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b) +``` + +is a kind of mapping function. It takes some kind of traversable data structure +of type `t a` containing values type `a`. It maps those values to operations of +type `f b` in some applicative functor using the given mapping function of type +`a -> f b`. Finally it returns an operation of type `f (t b)` that constructs a +new data structure of type `t b`. + +The optical version of `traverse` replaces the second class `Traversable` +constructor class with a first class traversal function + +```haskell +type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t +``` + +and `traverse` using an optic merely calls the given traversal function + +```haskell +traverse :: Applicative f => (a -> f b) -> Traversal s t a b -> s -> f t +traverse a2bF o = o a2bF +``` + +A traversal function of type `Traversal s t a b` is simply a function that knows +how to locate elements of type `a` within a data structure of type `s` and then +knows how to build a new data structure of type `t` where values of type `a` +have been replaced with values of type `b`. In other words, the traversal +function knows how to both take apart a data structure in a particular way to +extract some values out of it and also how to put the data structure back +together substituting some new values for the extracted values. Of course, it +is often the case that the type `b` is the same type `a` and type `t` is the +same as `s`. + +We can translate the above `traverse` function to JavaScript in [Static +Land](https://github.com/rpominov/static-land/blob/master/docs/spec.md) style by +passing the method dictionary corresponding to the `Applicative` constraint as +an explicit argument: + +```js +const traverse = F => a2bF => o => o(F)(a2bF) +``` + +Innocent as it may seem, *every* operation in Partial Lenses is basically an +application of a traversal function like that. The Partial Lenses version of +[`traverse`](README.md#L-traverse) is only slightly different due to currying, +built-in indexing, and the lifting of strings, numbers, and arrays to optics. + +Here is an example of an `elems` traversal over the elements of an array: + +```js +const elems = A => x2yA => xs => xs.reduce( + (ysA, x) => A.ap(A.map(ys => y => [...ys, y], ysA), x2yA(x)), + A.of([]) +) +``` + +Above, `A` is a Static Land [applicative +functor](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative), +`x2yA` is the function mapping array elements to applicative operations, and +`xs` is an array. + +To actually use `elems` with `traverse` we need an applicative functor. Perhaps +the most straightforward example is using the identity applicative: + +```js +const Identity = {map: (x2y, x) => x2y(x), ap: (x2y, x) => x2y(x), of: x => x} +``` + +The identity applicative performs no interesting computation by itself. Any +value is taken as such and both `map` and `ap` simply apply the second argument +to the first argument. + +By supplying the `Identity` applicative to `traverse` we get a mapping function +over a given traversal: + +```js +const map = traverse(Identity) +``` + +In Partial Lenses the above function is called [`modify`](README.md#L-modify) +and it takes its arguments in a different order, but otherwise it is the same. + +Using `map` and `elems` we can now map over an array of elements: + +```js +map(x => x + 1)(elems)([3, 1, 4]) +// [4, 2, 5] +``` + +At this point we basically have a horribly complex version of the map function +for arrays. Notice, however, that `map` takes the optic, `elems` in the above +case, as an argument. We can compose optics and get different behavior. + +The following `o` function composes two optics `outer` and `inner`: + +```js +const o = (outer, inner) => C => x2yC => outer(C)(inner(C)(x2yC)) +``` + +If you look closely, you'll notice that the above function really is just a +variation of ordinary function composition. Consider what we get if we drop the +`C` argument: + +```jsx +const o = (outer, inner) => x2yC => outer (inner (x2yC)) +``` + +That is exactly the same as ordinary single argument function composition. + +We can also define an identity optic function: + +```js +const identity = C => x2yC => x => x2yC(x) +``` + +And a function to compose any number of optics: + +```js +const compose = optics => optics.reduce(o, identity) +``` + +Using `compose` we can now conveniently map over nested arrays: + +```js +map(x => x + 1)(compose([elems, elems, elems]))([[[1]], [[2, 3], [4]]]) +// [[[2]], [[3, 4], [5]]] +``` + +Let's then divert our attention to lenses for a moment. One could say that +lenses are just traversals that focus on exactly one element. Let's build +lenses for accessing array elements and object properties. We can do so in a +generalized manner by introducing `Ix` modules with `get` and `set` functions +for both arrays and objects: + +```js +const ArrayIx = { + set: (i, v, a) => [...a.slice(0, i), v, ...a.slice(i+1)], + get: (i, a) => a[i] +} + +const ObjectIx = { + set: (n, v, o) => ({...o, [n]: v}), + get: (n, o) => o[n] +} +``` + +The `atOf` function then takes an `Ix` module and a key and return a lens: + +```js +const atOf = Ix => k => F => x2yF => x => F.map( + y => Ix.set(k, y, x), + x2yF(Ix.get(k, x)) +) +``` + +Notice that we only use the `map` function from the `F` functor argument. In +other words, lenses do not require an applicative functor. Lenses only require +a functor. Otherwise lens functions are just like traversal functions. + +As a convenience the `at` function dispatches to `atOf` so that when the key is +a number it uses array indexing and otherwise object indexing: + +```js +const at = k => atOf(typeof k === 'number' ? ArrayIx : ObjectIx)(k) +``` + +We can now map over e.g. an object property: + +```js +map(x => -x)(at('b'))({a: 1, b: 2, c: 3}) +// {a: 1, b: -2, c: 3} +``` + +We can also compose lens and traversal functions. For example: + +```js +map(x => -x)(compose([elems, at('x')]))([{x: 1}, {x: 2}]) +// [{x: -1}, {x: -2}] +``` + +```js +map(x => x.toUpperCase())(compose([at('xs'), elems]))({xs: ['a', 'b']}) +// {xs: ['A', 'B']} +``` + +Composing two lenses gives a lens. Composing a lens and a traversal gives a +traversal. And composing two traversals gives a traversal. + +We have so far only used the identity applicative. By using other algebras we +get different operations. One suitable algebra is the constant functor: + +```js +const Constant = {map: (x2y, c) => c} +``` + +The constant functor is a somewhat strange beast. The `map` function of the +constant functor simply ignores the first argument and returns the second +argument as is. This basically means that after a value is injected into the +constant functor it never changes. We can use that to create a `get` function + +```js +const get = traverse(Constant)(x => x) +``` + +that extracts the element targeted by a lens without building a new data +structure during the traversal. Recall that the `map` function of the +`Constant` functor actually does not use the given mapping function at all. + +For example: + +```js +get(compose([at(1), at('x')]))([{x: 1}, {x: 2}, {x: 3}]) +// 2 +``` + +The same lens, e.g. `compose([at(1), at('x')])`, can now be used to both `get` +and `map` over the targeted element. + +The constant functor cannot be used with traversal functions, because traversal +functions like `elems` require an applicative functor with not just the `map` +function, but also the `ap` and `of` functions. We can build applicatives +similar to the constant functor from +[monoids](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monoid) +and use those to fold over the elements targeted by a traversal: + +```js +const foldWith = M => traverse({...Constant, ap: M.concat, of: _ => M.empty()}) +``` + +The above `foldWith` function takes a Static Land +[monoid](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monoid) +and creates a applicative whose `ap` and `of` methods essentially ignore their +arguments and use the monoid. + +Using different monoids we get different operations. For example, we can define +an operation to collect all the elements targeted by a traversal: + +```js +const collect = foldWith({empty: () => [], concat: (l, r) => [...l, ...r]})(x => [x]) +``` + +```js +collect(compose([at('xs'), elems, at('x')]))({xs: [{x: 3}, {x: 1}, {x: 4}]}) +// [3, 1, 4] +``` + +Ans we can define an operation to sum all the elements targeted by a traversal: + +```js +const sum = foldWith({empty: () => 0, concat: (x, y) => x + y})(x => x) +``` + +```js +sum(compose([at('xs'), elems, at('x')]))({xs: [{x: 3}, {x: 1}, {x: 4}]}) +// 8 +``` + +This pretty much covers the basics of lenses and traversals. The Partial Lenses +library simply provides you with a large number of predefined lens and traversal +functions and operations, such as folds, over optics. diff --git a/docs/implementation.html b/docs/implementation.html new file mode 100644 index 00000000..b037c9e2 --- /dev/null +++ b/docs/implementation.html @@ -0,0 +1,238 @@ + + + + + Partial Lenses Implementation + + + + + + + + + +
+ Please wait... The interactive code snippets on this page take a moment to render. +
+ +

+ All the code snippets on this page are live and interactive + powered by the klipse + plugin. +

+
+

Partial Lenses Implementation · Gitter GitHub stars npm

+

This document describes a simplified implementation of lenses and traversals +using a similar approach as Partial Lenses. The implementation of Partial +Lenses is far from simplified. It lifts strings, numbers, and arrays to optics +for notational convenience, it has been manually tweaked for size, optimized for +performance, and it also tries to handle a lot of corner cases induced by +JavaScript. All of this makes the implementation difficult to understand on its +own. The intention behind this document is to describe a simple implementation +based on which it should be easier to look at the Partial Lenses source code and +understand what is going on.

+

There are many approaches to optics. Partial Lenses is based on the ideas +described by Twan van Laarhoven in CPS based functional +references and +further by Russell O'Connor in Polymorphic Update with van Laarhoven +Lenses.

+

One way to think of lenses and traversals is as being an application of a single +generalized traverse function. The +traverse +function of the Traversable constructor class

+
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
+
+

is a kind of mapping function. It takes some kind of traversable data structure +of type t a containing values type a. It maps those values to operations of +type f b in some applicative functor using the given mapping function of type +a -> f b. Finally it returns an operation of type f (t b) that constructs a +new data structure of type t b.

+

The optical version of traverse replaces the second class Traversable +constructor class with a first class traversal function

+
type Traversal s t a b = forall f. Applicative f => (a -> f b) -> s -> f t
+
+

and traverse using an optic merely calls the given traversal function

+
traverse :: Applicative f => (a -> f b) -> Traversal s t a b -> s -> f t
+traverse a2bF o = o a2bF
+
+

A traversal function of type Traversal s t a b is simply a function that knows +how to locate elements of type a within a data structure of type s and then +knows how to build a new data structure of type t where values of type a +have been replaced with values of type b. In other words, the traversal +function knows how to both take apart a data structure in a particular way to +extract some values out of it and also how to put the data structure back +together substituting some new values for the extracted values. Of course, it +is often the case that the type b is the same type a and type t is the +same as s.

+

We can translate the above traverse function to JavaScript in Static +Land style by +passing the method dictionary corresponding to the Applicative constraint as +an explicit argument:

+
var traverse = F => a2bF => o => o(F)(a2bF)
+
+

Innocent as it may seem, every operation in Partial Lenses is basically an +application of a traversal function like that. The Partial Lenses version of +traverse is only slightly different due to currying, +built-in indexing, and the lifting of strings, numbers, and arrays to optics.

+

Here is an example of an elems traversal over the elements of an array:

+
var elems = A => x2yA => xs => xs.reduce(
+  (ysA, x) => A.ap(A.map(ys => y => [...ys, y], ysA), x2yA(x)),
+  A.of([])
+)
+
+

Above, A is a Static Land applicative +functor, +x2yA is the function mapping array elements to applicative operations, and +xs is an array.

+

To actually use elems with traverse we need an applicative functor. Perhaps +the most straightforward example is using the identity applicative:

+
var Identity = {map: (x2y, x) => x2y(x), ap: (x2y, x) => x2y(x), of: x => x}
+
+

The identity applicative performs no interesting computation by itself. Any +value is taken as such and both map and ap simply apply the second argument +to the first argument.

+

By supplying the Identity applicative to traverse we get a mapping function +over a given traversal:

+
var map = traverse(Identity)
+
+

In Partial Lenses the above function is called modify +and it takes its arguments in a different order, but otherwise it is the same.

+

Using map and elems we can now map over an array of elements:

+
map(x => x + 1)(elems)([3, 1, 4])
+
+

At this point we basically have a horribly complex version of the map function +for arrays. Notice, however, that map takes the optic, elems in the above +case, as an argument. We can compose optics and get different behavior.

+

The following o function composes two optics outer and inner:

+
var o = (outer, inner) => C => x2yC => outer(C)(inner(C)(x2yC))
+
+

If you look closely, you'll notice that the above function really is just a +variation of ordinary function composition. Consider what we get if we drop the +C argument:

+
var o = (outer, inner) =>      x2yC => outer   (inner   (x2yC))
+
+

That is exactly the same as ordinary single argument function composition.

+

We can also define an identity optic function:

+
var identity = C => x2yC => x => x2yC(x)
+
+

And a function to compose any number of optics:

+
var compose = optics => optics.reduce(o, identity)
+
+

Using compose we can now conveniently map over nested arrays:

+
map(x => x + 1)(compose([elems, elems, elems]))([[[1]], [[2, 3], [4]]])
+
+

Let's then divert our attention to lenses for a moment. One could say that +lenses are just traversals that focus on exactly one element. Let's build +lenses for accessing array elements and object properties. We can do so in a +generalized manner by introducing Ix modules with get and set functions +for both arrays and objects:

+
var ArrayIx = {
+  set: (i, v, a) => [...a.slice(0, i), v, ...a.slice(i+1)],
+  get: (i, a) => a[i]
+}
+
+var ObjectIx = {
+  set: (n, v, o) => ({...o, [n]: v}),
+  get: (n, o) => o[n]
+}
+
+

The atOf function then takes an Ix module and a key and return a lens:

+
var atOf = Ix => k => F => x2yF => x => F.map(
+  y => Ix.set(k, y, x),
+  x2yF(Ix.get(k, x))
+)
+
+

Notice that we only use the map function from the F functor argument. In +other words, lenses do not require an applicative functor. Lenses only require +a functor. Otherwise lens functions are just like traversal functions.

+

As a convenience the at function dispatches to atOf so that when the key is +a number it uses array indexing and otherwise object indexing:

+
var at = k => atOf(typeof k === 'number' ? ArrayIx : ObjectIx)(k)
+
+

We can now map over e.g. an object property:

+
map(x => -x)(at('b'))({a: 1, b: 2, c: 3})
+
+

We can also compose lens and traversal functions. For example:

+
map(x => -x)(compose([elems, at('x')]))([{x: 1}, {x: 2}])
+
+
map(x => x.toUpperCase())(compose([at('xs'), elems]))({xs: ['a', 'b']})
+
+

Composing two lenses gives a lens. Composing a lens and a traversal gives a +traversal. And composing two traversals gives a traversal.

+

We have so far only used the identity applicative. By using other algebras we +get different operations. One suitable algebra is the constant functor:

+
var Constant = {map: (x2y, c) => c}
+
+

The constant functor is a somewhat strange beast. The map function of the +constant functor simply ignores the first argument and returns the second +argument as is. This basically means that after a value is injected into the +constant functor it never changes. We can use that to create a get function

+
var get = traverse(Constant)(x => x)
+
+

that extracts the element targeted by a lens without building a new data +structure during the traversal. Recall that the map function of the +Constant functor actually does not use the given mapping function at all.

+

For example:

+
get(compose([at(1), at('x')]))([{x: 1}, {x: 2}, {x: 3}])
+
+

The same lens, e.g. compose([at(1), at('x')]), can now be used to both get +and map over the targeted element.

+

The constant functor cannot be used with traversal functions, because traversal +functions like elems require an applicative functor with not just the map +function, but also the ap and of functions. We can build applicatives +similar to the constant functor from +monoids +and use those to fold over the elements targeted by a traversal:

+
var foldWith = M => traverse({...Constant, ap: M.concat, of: _ => M.empty()})
+
+

The above foldWith function takes a Static Land +monoid +and creates a applicative whose ap and of methods essentially ignore their +arguments and use the monoid.

+

Using different monoids we get different operations. For example, we can define +an operation to collect all the elements targeted by a traversal:

+
var collect = foldWith({empty: () => [], concat: (l, r) => [...l, ...r]})(x => [x])
+
+
collect(compose([at('xs'), elems, at('x')]))({xs: [{x: 3}, {x: 1}, {x: 4}]})
+
+

Ans we can define an operation to sum all the elements targeted by a traversal:

+
var sum = foldWith({empty: () => 0, concat: (x, y) => x + y})(x => x)
+
+
sum(compose([at('xs'), elems, at('x')]))({xs: [{x: 3}, {x: 1}, {x: 4}]})
+
+

This pretty much covers the basics of lenses and traversals. The Partial Lenses +library simply provides you with a large number of predefined lens and traversal +functions and operations, such as folds, over optics.

+ +
+

+        document.querySelector('.loading-message').className = "loading-hidden";
+        ga('send', 'event', 'completed', 'load', Math.round((Date.now() - startTime)/1000));
+        accelerate_klipse();
+      
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/klipse-github-docs.config.js b/klipse-github-docs.config.js index bdba6d20..65fadd6b 100644 --- a/klipse-github-docs.config.js +++ b/klipse-github-docs.config.js @@ -10,22 +10,33 @@ 'partial.lenses.js', 'https://unpkg.com/ramda/dist/ramda.min.js', 'https://unpkg.com/immutable/dist/immutable.min.js', - 'https://unpkg.com/moment/min/moment.min.js', + 'https://unpkg.com/moment/min/moment.min.js' ] } - return [Object.assign({}, targetDefaults, { - source: 'README.md', - target: 'index.html', - title: 'Partial Lenses', - stripComments: true, - constToVar: true, - menu: true, - tooltips: true - }), Object.assign({}, targetDefaults, { - source: 'EXERCISES.md', - target: 'exercises.html', - title: 'Partial Lenses Exercises', - menu: true - })] + return [ + Object.assign({}, targetDefaults, { + source: 'README.md', + target: 'index.html', + title: 'Partial Lenses', + stripComments: true, + constToVar: true, + menu: true, + tooltips: true + }), + Object.assign({}, targetDefaults, { + source: 'EXERCISES.md', + target: 'exercises.html', + title: 'Partial Lenses Exercises', + menu: true + }), + Object.assign({}, targetDefaults, { + source: 'IMPLEMENTATION.md', + target: 'implementation.html', + title: 'Partial Lenses Implementation', + stripComments: true, + constToVar: true, + menu: true + }) + ] }