Repository: calmm-js/partial.lenses
Pull request #72 (2017-04-07 11:22:32)

Add `traverse`

codecov-io: # [Codecov](https://codecov.io/gh/calmm-js/partial.lenses/pull/72?src=pr&el=h1) Report
> Merging [#72](https://codecov.io/gh/calmm-js/partial.lenses/pull/72?src=pr&el=desc) into [master](https://codecov.io/gh/calmm-js/partial.lenses/commit/86ce235388307b6040b6d835d15ad5a1337dbcab?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/calmm-js/partial.lenses/pull/72/graphs/tree.svg?height=150&token=7XG6Y1OWnL&src=pr&width=650)](https://codecov.io/gh/calmm-js/partial.lenses/pull/72?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #72   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           1      1           
  Lines         450    451    +1     
=====================================
+ Hits          450    451    +1
```


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

------

[Continue to review full report at Codecov](https://codecov.io/gh/calmm-js/partial.lenses/pull/72?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/72?src=pr&el=footer). Last update [86ce235...e132a35](https://codecov.io/gh/calmm-js/partial.lenses/pull/72?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 d241488..eabb7f1 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ parts.  [Try Lenses!](https://calmm-js.github.io/partial.lenses/playground.html)
       * [`L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData`](#L-modify "L.modify: POptic s a -> ((Maybe a, Index) -> Maybe a) -> Maybe s -> Maybe s")
       * [`L.remove(optic, maybeData) ~> maybeData`](#L-remove "L.remove: POptic s a -> Maybe s -> Maybe s")
       * [`L.set(optic, maybeValue, maybeData) ~> maybeData`](#L-set "L.set: POptic s a -> Maybe a -> Maybe s -> Maybe s")
+      * [`L.traverse(category, (maybeValue, index) => operation, optic, maybeData) ~> operation`](#L-traverse "L.traverse: (Functor|Applicative|Monad) c -> ((Maybe a, Index) -> c b) -> POptic s t a b -> Maybe s -> c t")
     * [Nesting](#nesting)
       * [`L.compose(...optics) ~> optic`](#L-compose "L.compose: (POptic s s1, ...POptic sN a) -> POptic s a") or `[...optics]`
     * [Querying](#querying)
@@ -781,6 +782,42 @@ of a data structure.
 Note that `L.set(lens, maybeValue, maybeData)` is equivalent
 to [`L.modify(lens, R.always(maybeValue), maybeData)`](#L-modify).
 
+##### <a id="L-traverse"></a> [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/#L-traverse) [`L.traverse(category, (maybeValue, index) => operation, optic, maybeData) ~> operation`](#L-traverse "L.traverse: (Functor|Applicative|Monad) c -> ((Maybe a, Index) -> c b) -> POptic s t a b -> Maybe s -> c t")
+
+`L.traverse` maps each focus to an operation and returns an operation that runs
+those operations in-order and collects the results.  The `category` argument
+must be either a `Functor`, `Applicative`, or `Monad` depending on the optic as
+specified in [`L.toFunction`](#L-function).
+
+Here is a bit involved example that uses the State monad and `L.traverse` to
+replace elements in a data structure by the number of times those elements have
+appeared at that point in the data structure:
+
+```js
+const Monad = ({of, chain}) => ({
+  of,
+  chain,
+  ap: (x2yS, xS) => chain(x2y => chain(x => of(x2y(x)), xS), x2yS),
+  map: (x2y, xS) => chain(x => of(x2y(x)), xS)
+})
+
+const StateM = Monad({
+  of: x => s => [x, s],
+  chain: (x2yS, xS) => s1 => {
+    const [x, s] = xS(s1)
+    return x2yS(x)(s)
+  }
+})
+
+const countS = x => x2n => {
+  const n = (x2n[x] || 0) + 1
+  return [n, R.assoc(x, n, x2n)]
+}
+
+L.traverse(StateM, countS, L.elems, [1, 2, 1, 1, 2, 3, 4, 3, 4, 5])({})[0]
+// [1, 1, 2, 3, 2, 1, 1, 2, 2, 1]
+```
+
 #### Nesting
 
 ##### <a id="L-compose"></a> [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/#L-compose) [`L.compose(...optics) ~> optic`](#L-compose "L.compose: (POptic s s1, ...POptic sN a) -> POptic s a") or `[...optics]`
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index 542605f..f639587 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -527,6 +527,8 @@ export const remove = curry((o, s) => setU(o, void 0, s))
 
 export const set = curry(setU)
 
+export const traverse = curry((C, xMi2yC, t, s) => run(t, C, xMi2yC, s))
+
 // Sequencing
 
 export function seq() {


Test patch:
diff --git a/test/core.js b/test/core.js
index fb373e9..f3788f0 100644
--- a/test/core.js
+++ b/test/core.js
@@ -38,6 +38,7 @@ export const toFunction = L.toFunction
 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 traverse = L.traverse
 
 // Sequencing
 
diff --git a/test/tests.js b/test/tests.js
index 005ac06..624ed31 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -78,6 +78,21 @@ const collectM = R.curry((o, s) =>
 
 //
 
+const StateM = Monad({
+  of: x => s => [x, s],
+  chain: (x2yS, xS) => s1 => {
+    const [x, s] = xS(s1)
+    return x2yS(x)(s)
+  }
+})
+
+const countS = x => x2n => {
+  const n = (x2n[x] || 0) + 1
+  return [n, R.assoc(x, n, x2n)]
+}
+
+//
+
 function show(x) {
   switch (typeof x) {
   case "string":
@@ -91,10 +106,12 @@ function show(x) {
 const run = expr => eval(`() => ${expr}`)(
   C,
   Sum,
+  StateM,
   T,
   XYZ,
   a100000,
   collectM,
+  countS,
   everywhere,
   flatten,
   id,
@@ -217,6 +234,7 @@ describe("arities", () => {
     slice: 2,
     sum: 2,
     toFunction: 1,
+    traverse: 4,
     valueOr: 1,
     values: 4,
     when: 1,
@@ -588,6 +606,15 @@ describe("L.concatAs", () => {
          6)
 })
 
+describe("L.traverse", () => {
+  testEq(`L.traverse(StateM,
+                     countS,
+                     flatten,
+                     [1, [[2, 1], 1], 2, [3, [[4]], [3, 4]], 5])({})`,
+         [[1, [[1, 2], 3], 2, [1, [[1]], [2, 2]], 1],
+          {1: 3, 2: 2, 3: 2, 4: 2, 5: 1}])
+})
+
 describe("folds", () => {
   testEq(`X.concat(Sum, X.elems, a100000)`, 100000)
   testEq(`X.concatAs(id, Sum, X.elems, a100000)`, 100000)
diff --git a/test/types.js b/test/types.js
index cedb97b..8236441 100644
--- a/test/types.js
+++ b/test/types.js
@@ -72,6 +72,11 @@ export const modify = T.fn([T_optic,
                            T_maybeDataO)
 export const remove = T.fn([T_optic, T_maybeDataI], T_maybeDataO)
 export const set = T.fn([T_optic, T_maybeDataI, T_maybeDataI], T_maybeDataO)
+export const traverse = T.fn([T.or(T_monad, T_applicative, T_functor),
+                              T.fn([T_maybeDataO, T_index], T.any),
+                              T_optic,
+                              T_maybeDataI],
+                             T.any)
 
 // Sequencing