diff --git a/README.md b/README.md
index 397eb784..3f05d26c 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,8 @@ parts. [Try Lenses!](https://calmm-js.github.io/partial.lenses/playground.html)
* [`L.transform(optic, maybeData) ~> maybeData`](#L-transform "L.transform: POptic s a -> Maybe s -> Maybe s") v11.7.0
* [Sequencing](#sequencing)
* [`L.seq(...transforms) ~> transform`](#L-seq "L.seq: (...PTransform s a) -> PTransform s a") v9.4.0
+ * [Backtracking](#backtracking)
+ * [`L.retry((maybeValue, index) => transform, transform) ~> transform`](#L-retry "L.retry: ((Maybe t, Index) -> PTransform s u) -> PTransform s t -> PTransform s u") v13.3.0
* [Transforming](#transforming)
* [`L.assignOp(object) ~> optic`](#L-assignOp "L.assignOp: {p1: a1, ...ps} -> POptic {p1: a1, ...ps, ...o} {p1: a1, ...ps}") v11.13.0
* [`L.modifyOp((maybeValue, index) => maybeValue) ~> optic`](#L-modifyOp "L.modifyOp: ((Maybe a, Index) -> Maybe a) -> POptic a a") v11.7.0
@@ -856,15 +858,16 @@ Now, optics are composable in several ways and in each of those ways there is an
operation to perform the composition and laws on how such composed optics
behave. Here is a table of the means of composition supported by this library:
-| | Operation(s) | Semantics
-| ------------------------- | ----------------------------------------------------------------------------------- | -----------------------------------------------------------------------------------------
-| [Nesting](#nesting) | [`L.compose(...optics)`](#L-compose) or `[...optics]` | [Monoid](https://en.wikipedia.org/wiki/Monoid) over [unityped](http://cs.stackexchange.com/questions/18847/if-dynamically-typed-languages-are-truly-statically-typed-unityped-languages-w) [optics](#optics)
-| [Recursing](#recursing) | [`L.lazy(optic => optic)`](#L-lazy) | [Fixed point](https://en.wikipedia.org/wiki/Fixed-point_combinator)
-| [Adapting](#adapting) | [`L.choices(optic, ...optics)`](#L-choices) | [Semigroup](https://en.wikipedia.org/wiki/Semigroup) over [optics](#optics)
-| [Querying](#querying) | [`L.choice(...optics)`](#L-choice) and [`L.chain(value => optic, optic)`](#L-chain) | [MonadPlus](https://en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus) over [optics](#optics)
-| Picking | [`L.pick({...prop:lens})`](#L-pick) | Product of [lenses](#lenses)
-| Branching | [`L.branch({...prop:traversal})`](#L-branch) | [Coproduct](https://en.wikipedia.org/wiki/Coproduct) of [traversals](#traversals)
-| [Sequencing](#sequencing) | [`L.seq(...transforms)`](#L-seq) | Monad over [transforms](#transforms)
+| | Operation(s) | Semantics
+| ----------------------------- | ----------------------------------------------------------------------------------- | -----------------------------------------------------------------------------------------
+| [Nesting](#nesting) | [`L.compose(...optics)`](#L-compose) or `[...optics]` | [Monoid](https://en.wikipedia.org/wiki/Monoid) over [unityped](http://cs.stackexchange.com/questions/18847/if-dynamically-typed-languages-are-truly-statically-typed-unityped-languages-w) [optics](#optics)
+| [Recursing](#recursing) | [`L.lazy(optic => optic)`](#L-lazy) | [Fixed point](https://en.wikipedia.org/wiki/Fixed-point_combinator)
+| [Adapting](#adapting) | [`L.choices(optic, ...optics)`](#L-choices) | [Semigroup](https://en.wikipedia.org/wiki/Semigroup) over [optics](#optics)
+| [Querying](#querying) | [`L.choice(...optics)`](#L-choice) and [`L.chain(value => optic, optic)`](#L-chain) | [MonadPlus](https://en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus) over [optics](#optics)
+| Picking | [`L.pick({...prop:lens})`](#L-pick) | Product of [lenses](#lenses)
+| Branching | [`L.branch({...prop:traversal})`](#L-branch) | [Coproduct](https://en.wikipedia.org/wiki/Coproduct) of [traversals](#traversals)
+| [Sequencing](#sequencing) | [`L.seq(...transforms)`](#L-seq) | Monad over [transforms](#transforms)
+| [Backtracking](#backtracking) | [`L.retry((maybeValue, index) => transform, transform)`](#L-retry) | Monad over [transforms](#transforms)
The above table and, in particular, the semantics column is by no means
complete. In particular, the documentation of this library does not generally
@@ -1381,18 +1384,23 @@ empty or read-only [zero](#L-zero).
`L.chain` provides a monadic
[chain](https://github.com/rpominov/static-land/blob/master/docs/spec.md#chain)
-combinator for querying with optics. `L.chain(toOptic, optic)` is equivalent to
+combinator for querying with optics. `L.chain(y2yzO, xyO)` is equivalent to
```jsx
-L.compose(optic, L.choose((maybeValue, index) =>
- maybeValue === undefined
+L.compose(xyO, L.choose((yMaybe, index) =>
+ yMaybe === undefined
? L.zero
- : toOptic(maybeValue, index)))
+ : y2yzO(yMaybe, index)))
```
Note that with the [`R.always`](http://ramdajs.com/docs/#always), `L.chain`,
[`L.choice`](#L-choice) and [`L.zero`](#L-zero) combinators, one can consider
-optics as subsuming the maybe monad.
+optics as providing a
+[Monad](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad)
+and
+[Alternative](https://github.com/rpominov/static-land/blob/master/docs/spec.md#alternative)
+aka
+[MonadPlus](https://en.wikibooks.org/wiki/Haskell/Alternative_and_MonadPlus#MonadPlus).
##### [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/index.html#L-choice) [`L.choice(...optics) ~> optic`](#L-choice "L.choice: (...POptic s a) -> POptic s a") v2.1.0
@@ -1645,11 +1653,42 @@ combined together as a
[`Monad`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad)
```jsx
-chain(x2t, t) = L.seq(t, L.choose(x2t))
- of(x) = L.setOp(x)
+chain(y2yzt, xyT) = L.seq(xyT, L.choose(y2yzT))
+ of(x) = L.setOp(x)
+```
+
+which is not the same as the [querying monad](#L-chain) or the [backtracking
+monad](#L-retry).
+
+#### [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/index.html#backtracking) Backtracking
+
+The [`L.retry`](#L-retry) combinator allows one to build
+[transforms](#transforms) that modify their focus in multiple different ways and
+choose one of the results.
+
+##### [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/index.html#L-retry) [`L.retry((maybeValue, index) => transform, transform) ~> transform`](#L-retry "L.retry: ((Maybe t, Index) -> PTransform s u) -> PTransform s t -> PTransform s u") v13.3.0
+
+`L.retry` creates a transform that first modifies the data in the focus with
+given transform and then modifies the same data in the focus with the transform
+returned by the given function.
+
+Note that `L.retry` with [`L.seqOp`](#L-setOp) forms another
+[`Monad`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad):
+
+```jsx
+chain(y2xyT, xyT) = L.retry(x2xyT, xyT)
+ of(x) = L.setOp(x)
```
-which is not the same as the [querying monad](#L-chain).
+which is not the same as the [querying monad](#L-chain) or the [sequencing
+monad](#L-seq).
+
+Note that `L.retry(t2suT, stT)` can be implemented using [`L.choose`](#L-choose)
+and [`L.seq`](#L-seq):
+
+```jsx
+L.choose((s, i) => L.seq(stT, L.choose(t => L.seq(L.setOp(s), t2suT(t, i)))))
+```
#### [≡](#contents) [▶](https://calmm-js.github.io/partial.lenses/index.html#transforming) Transforming
diff --git a/bench/bench.js b/bench/bench.js
index 29f28950..4fefc8bb 100644
--- a/bench/bench.js
+++ b/bench/bench.js
@@ -544,6 +544,15 @@ R.forEach(
L.seq(L.modifyOp(dec),
L.choose(n => n === 0 ? L.identity : rec))),
100)`
+ ],
+ [
+ `L.transform(I.seq(L.identity,
+ L.retry(L.setOp),
+ L.retry(L.setOp),
+ L.retry(L.setOp),
+ L.retry(L.setOp),
+ L.retry(L.setOp)),
+ 101)`
]
]
)
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index 86a29d6a..b26f3c04 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -894,6 +894,21 @@ export const seq = (process.env.NODE_ENV === 'production'
return r
})
+// Backtracking
+
+export const retry = (process.env.NODE_ENV === 'production'
+ ? I.curry
+ : C.res(C.par(2, C.ef(reqMonad('retry')))))(
+ (yi2xzT, xyT) => (
+ (xyT = toFunction(xyT)),
+ (x, i, M, xi2yM) =>
+ M.chain(
+ y => toFunction(yi2xzT(y, i))(x, i, M, xi2yM),
+ xyT(x, i, M, xi2yM)
+ )
+ )
+)
+
// Creating new traversals
export const branchOr = (process.env.NODE_ENV === 'production'
diff --git a/test/tests.js b/test/tests.js
index b1de0545..beb3a369 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -284,6 +284,7 @@ describe('arities', () => {
replace: 2,
required: 1,
reread: 1,
+ retry: 2,
reverse: 4,
rewrite: 1,
seemsArrayLike: 1,
@@ -1211,6 +1212,16 @@ describe('L.seq', () => {
testEq(`collectM(L.seq(1, 0, 2), ["b", "a", "c"])`, ['a', 'b', 'c'])
})
+describe('L.retry', () => {
+ testEq(
+ `L.transform([L.values,
+ L.retry((x, i) => L.modifyOp((y, i) => [{x, i}, {y, i}]),
+ L.modifyOp((x, i) => ({x, i})))],
+ {x: 42})`,
+ {x: [{x: {x: 42, i: 'x'}, i: 'x'}, {y: 42, i: 'x'}]}
+ )
+})
+
describe('lazy folds', () => {
testEq(`L.select(flatten, [[[[[[[[[[101]]]]]]]]]])`, 101)
testEq(`L.select(L.elems, [])`, undefined)
diff --git a/test/types.js b/test/types.js
index 18328f57..17cfdf2b 100644
--- a/test/types.js
+++ b/test/types.js
@@ -137,6 +137,13 @@ export const log = T.fnVarN(0, T.string, T_optic)
export const seq = T.fnVarN(0, T_optic, T_transform)
+// Backtracking
+
+export const retry = T.fn(
+ [T.fn([T_maybeDataO, T_index], T_transform), T_transform],
+ T_transform
+)
+
// Creating new traversals
export const branchOr = T.fn([T_optic, template(T_traversal)], T_traversal)