Repository: calmm-js/partial.lenses
Pull request #53 (2017-02-02 23:21:15)

Experimental transforms with monadic sequencing
PR to get CI feedback. Not to be merged lightly!

codecov-io: # [Codecov](https://codecov.io/gh/calmm-js/partial.lenses/pull/53?src=pr&el=h1) Report
> Merging [#53](https://codecov.io/gh/calmm-js/partial.lenses/pull/53?src=pr&el=desc) into [master](https://codecov.io/gh/calmm-js/partial.lenses/commit/f033968cafba4a25013b01de1aaf854cdddb6a1a?src=pr&el=desc) will **not impact** coverage.


```diff
@@          Coverage Diff          @@
##           master    #53   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           1      1           
  Lines         386    394    +8     
=====================================
+ Hits          386    394    +8
```


| [Impacted Files](https://codecov.io/gh/calmm-js/partial.lenses/pull/53?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/partial.lenses.js](https://codecov.io/gh/calmm-js/partial.lenses/compare/f033968cafba4a25013b01de1aaf854cdddb6a1a...23130e8bfe97f7e70f2963d83af65c76935f13d9?src=pr&el=tree#diff-c3JjL3BhcnRpYWwubGVuc2VzLmpz) | `100% <100%> (ø)` | :white_check_mark: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/calmm-js/partial.lenses/pull/53?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/calmm-js/partial.lenses/pull/53?src=pr&el=footer). Last update [f033968...23130e8](https://codecov.io/gh/calmm-js/partial.lenses/compare/f033968cafba4a25013b01de1aaf854cdddb6a1a...23130e8bfe97f7e70f2963d83af65c76935f13d9?el=footer&src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Patch:
diff --git a/README.md b/README.md
index 5e38999..1123863 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,9 @@ parts.  [▶ Try Lenses!](https://calmm-js.github.io/partial.lenses/)
       * [`L.log(...labels) ~> optic`](#L-log "L.log: (...Any) -> POptic s s")
     * [Internals](#internals)
       * [`L.toFunction(optic) ~> optic`](#L-toFunction "L.toFunction: POptic s a -> ((Functor|Applicative) c, (Maybe a, Index) -> c b, Maybe s, Index) -> c t")
+  * [Transforms](#transforms)
+    * [Sequencing](#sequencing)
+      * [`L.seq(...optics) ~> transform`](#L-seq "L.seq: (...POptic s a) -> PTransform s a")
   * [Traversals](#traversals)
     * [Operations on traversals](#operations-on-traversals)
       * [`L.concat(monoid, traversal, maybeData) ~> traversal`](#L-concat "L.concat: Monoid a -> (PTraversal s a -> Maybe s -> a)")
@@ -655,8 +658,9 @@ 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 at nested positions meaning that
-the recursive use is precomposed with some other optic.
+in a meaningful way when the recursive uses are either [precomposed](#L-compose)
+or [presequenced](#L-seq) 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:
@@ -737,27 +741,83 @@ will have the signature
 (Functor c, (Maybe a, Index) -> c b, Maybe s, Index) -> c t
 ```
 
-and for [traversals](#traversals) the signature will be
+for [traversals](#traversals) the signature will be
 
 ```jsx
 (Applicative c, (Maybe a, Index) -> c b, Maybe s, Index) -> c t
 ```
 
+and for [transforms](#transforms) the signature will be
+
+```jsx
+(Monad c, (Maybe a, Index) -> c b, Maybe s, Index) -> c t
+```
+
 Note that the above signatures are written using the "tupled" parameter notation
 `(...) -> ...` to denote that the functions are not curried.
 
 The
-[`Functor`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#functor) and
-[`Applicative`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative) arguments
+[`Functor`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#functor),
+[`Applicative`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative),
+and
+[`Monad`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) arguments
 are expected to conform to
 their
 [Static Land](https://github.com/rpominov/static-land/blob/master/docs/spec.md)
 specifications.
 
 Note that, in conjunction with partial optics, it may be advantageous to have
-the `Functor` and `Applicative` algebras to allow for partiality.  With
-traversals it is also possible, for example, to simply post compose optics
-with [`L.optional`](#L-optional) to eliminate `undefined` elements.
+the algebras to allow for partiality.  With traversals it is also possible, for
+example, to simply post compose optics with [`L.optional`](#L-optional) to
+skip `undefined` elements.
+
+### Transforms
+
+A transform operates over focuses that may overlap and may be visited multiple
+times.  This allows operations that are impossible to implement using other
+optics, but also potentially makes it much more difficult to reason about the
+results.  Transforms can only be [modified](#L-modify), [set](#L-set)
+and [removed](#L-remove).
+
+#### Sequencing
+
+##### <a name="L-seq"></a> [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/#L-seq) [`L.seq(...optics) ~> transform`](#L-seq "L.seq: (...POptic s a) -> PTransform s a")
+
+`L.seq` creates a transform that modifies the focus with each of the given
+optics in sequence.
+
+For example:
+
+```js
+L.modify(L.seq(L.identity, L.identity, L.identity), x => [x], 1)
+// [ [ [ 1 ] ] ]
+```
+
+Here is an example of a bottom-up transform over a data structure of nested
+objects and arrays:
+
+```js
+const everywhere = [L.optional, L.lazy(rec => {
+  const elems = [L.elems, rec]
+  const values = [L.values, rec]
+  return L.seq(L.choose(x => (x instanceof Array ? elems :
+                              x instanceof Object ? values :
+                              L.zero)),
+               L.identity)
+})]
+```
+
+The above `everywhere` transform is similar to
+the [`F.everywhere`](https://github.com/polytypic/fastener#F-everywhere)
+transform of the [`fastener`](https://github.com/polytypic/fastener)
+zipper-library.  Note that the above `everywhere` and the [`flatten`](#L-lazy)
+example differ in that `flatten` only targets the non-object and non-array
+elements of the data structure while `everywhere` also targets those.
+
+```js
+L.modify(everywhere, x => [x], {xs: [{x: 1}, {x: 2}]})
+// [ {xs: [ [ [ { x: [ 1 ] } ], [ { x: [ 2 ] } ] ] ] } ]
+```
 
 ### Traversals
 
diff --git a/bench/bench.js b/bench/bench.js
index a5ddb52..51616be 100644
--- a/bench/bench.js
+++ b/bench/bench.js
@@ -95,6 +95,19 @@ const values = L.lazy(rec => [
 const bstPairs = [[3, "g"], [2, "a"], [1, "m"], [4, "i"], [5, "c"]]
 const bst = fromPairs(bstPairs)
 
+const incNum = x => typeof x === "number" ? x + 1 : x
+const nested = [{x:1,y:[2,{d:3},4],z:{a:5}}]
+const everywhere = [L.optional, L.lazy(rec => {
+  const elems = L.toFunction([L.elems, rec])
+  const values = L.toFunction([L.values, rec])
+  return L.seq(L.choose(x => (x instanceof Array ? elems :
+                              x instanceof Object ? values :
+                              L.zero)),
+               L.identity)
+})]
+
+const xyzs = L.seq("x","y","z")
+
 const Benchmark = require("benchmark")
 Benchmark.options.maxTime = Number(process.argv[2]) || Benchmark.options.maxTime
 
@@ -245,6 +258,21 @@ R.forEach(bs => {
     `L.modify(L.values, inc, xs100o)`,
     `L.modify(L.values, inc, xs1000o)`,
     `L.modify(L.values, inc, xs10000o)`,
+  ], [
+    `L.modify(everywhere, incNum, nested)`,
+    `L.modify(flatten, inc, nested)`,
+  ], [
+    `L.modify(everywhere, incNum, xs10)`,
+    `L.modify(flatten, inc, xs10)`,
+  ], [
+    `L.modify(everywhere, incNum, xs100)`,
+    `L.modify(flatten, inc, xs100)`,
+  ], [
+    `L.modify(everywhere, incNum, xs1000)`,
+    `L.modify(flatten, inc, xs1000)`,
+  ], [
+    `L.set(L.seq("x","y","z"), 1, undefined)`,
+    `L.set(xyzs, 1, undefined)`,
   ], [
     `L.modify(values, x => x + x, bst)`,
   ], [
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index d51ece3..71465d7 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -56,7 +56,7 @@ function copyToFrom(ys, k, xs, i, j) {
 
 //
 
-const Ident = {map: applyU, ap: applyU, of: id}
+const Ident = {map: applyU, of: id, ap: applyU, chain: applyU}
 
 const Const = {map: sndU}
 
@@ -448,6 +448,22 @@ export const remove = curry((o, s) => setU(o, void 0, s))
 
 export const set = curry(setU)
 
+// Sequencing
+
+export function seq() {
+  const n = arguments.length, xMs = Array(n)
+  for (let i=0; i<n; ++i)
+    xMs[i] = toFunction(arguments[i])
+  const loop = (M, xi2xM, i, j) => j === n
+    ? M.of
+    : x => (0, M.chain)(loop(M, xi2xM, i, j+1), xMs[j](M, xi2xM, x, i))
+  return (M, xi2xM, x, i) => {
+    if (process.env.NODE_ENV !== "production" && !M.chain)
+      errorGiven("`seq` requires a monad", M)
+    return loop(M, xi2xM, i, 0)(x)
+  }
+}
+
 // Nesting
 
 export function compose() {


Test patch:
diff --git a/test/core.js b/test/core.js
index 6d461c6..ec2d12b 100644
--- a/test/core.js
+++ b/test/core.js
@@ -30,6 +30,8 @@ export const modify = L.modify
 export const remove = R.curry((o, s) => set(o, undefined, s))
 export const set = R.curry((o, x, s) => modify(o, R.always(x), s))
 
+export const seq = L.seq
+
 export const compose = L.compose
 
 export const chain = R.curry((x2yO, xO) =>
diff --git a/test/tests.js b/test/tests.js
index d64a948..9978d53 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -632,6 +632,8 @@ if (process.env.NODE_ENV !== "production") {
     testThrows('L.set(null, 1, 2)')
 
     testThrows('L.toFunction((one, too, many) => 1)')
+
+    testThrows(`L.get(L.seq(0), ["x"])`)
   })
 }
 
@@ -683,3 +685,19 @@ describe("BST", () => {
          I.seq([["m",-1], ["a",-2], ["g",-3], ["i",-4], ["c",-5]],
                BST.fromPairs))
 })
+
+export const everywhere = [L.optional, L.lazy(rec => {
+  const elems = [L.elems, rec]
+  const values = [L.values, rec]
+  return L.seq(L.choose(x => (x instanceof Array ? elems :
+                              x instanceof Object ? values :
+                              L.zero)),
+               L.identity)
+})]
+
+describe("seq", () => {
+  testEq(`L.set(L.seq(), "ignored", "anything")`, "anything")
+  testEq(`L.set([L.seq(), "x"], "ignored", {x: "anything"})`, {x: "anything"})
+  testEq(`L.set(L.seq("x", "y", "z"), 1, undefined)`, {x:1,y:1,z:1})
+  testEq(`L.modify(everywhere, x => [x], {x: {y: 1}})`, [{x: [{y: [1]}]}])
+})
diff --git a/test/types.js b/test/types.js
index db7ebb9..6b07851 100644
--- a/test/types.js
+++ b/test/types.js
@@ -15,6 +15,13 @@ const T_applicative = T.object({
   ap: T.fn([T.any, T.any], T.any)
 })
 
+const T_monad = T.object({
+  chain: T.fn([T.fn([T.any], T.any), T.any], T.any),
+  map: T.fn([T.fn([T.any], T.any), T.any], T.any),
+  of: T.fn([T.any], T.any),
+  ap: T.fn([T.any, T.any], T.any)
+})
+
 const T_opticFnOf = Category =>
   T.fn([Category,
         T.fn([T_maybeData, T_index], T_maybeData),
@@ -29,9 +36,10 @@ const T_opticOf = Category => T.lazy(T_optic => T.or(
   T.fn([T_maybeData, T_index], T_maybeData),
   T_opticFnOf(Category)))
 
-const T_optic = T_opticOf(T.or(T_applicative, T_functor))
+const T_optic = T_opticOf(T.or(T_monad, T_applicative, T_functor))
 
-const T_traversal = T_opticOf(T_applicative)
+const T_transform = T_opticOf(T_monad)
+const T_traversal = T_opticOf(T.or(T_monad, T_applicative))
 const T_lens = T_optic
 const T_isomorphism = T_lens
 
@@ -47,6 +55,8 @@ export const modify = T.fn([T_optic,
 export const remove = T.fn([T_optic, T_maybeData], T_maybeData)
 export const set = T.fn([T_optic, T_maybeData, T_maybeData], T_maybeData)
 
+export const seq = T.fnVar(T_optic, T_transform)
+
 export const compose = T.fnVar(T_optic, T_optic)
 
 export const chain = T.fn([T.fn([T_data, T_index], T_optic), T_lens], T_optic)