Repository: calmm-js/partial.lenses
Pull request #19 (2016-04-08 06:18:07)

Associative choice with identity.
This corrects the `L.firstOf` mistake where I didn't see the possibility
of a suitable identity element for choice.  `L.nothing` acts like the
identity element of `L.choice`.  `L.orElse` can be used to give a backup
or default if it is really needed, but none of the actual use cases I've
seen so far require a backup.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8bd77e..9f1b03b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 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
diff --git a/README.md b/README.md
index f6899a1..53fb2ca 100644
--- a/README.md
+++ b/README.md
@@ -472,6 +472,13 @@ L.over(majorAxis, R.negate, {x: 2, y: -3})
 // { y: 3, x: 2 }
 ```
 
+#### [`L.choice(...ls)`](#lchoicels "L.choice :: (...PLens s a) -> PLens s a")
+
+`L.choice(...ls)` returns a partial lens that acts like the first of the given
+lenses, `...ls`, whose view is not undefined on the given target.  When the
+views of all of the given lenses are undefined, the returned lens acts like
+`L.nothing`, which is the identity element of `L.choice`.
+
 #### [`L.compose(l, ...ls)`](#lcomposel-ls "L.compose :: (PLens s s1, ...PLens sN a) -> PLens s a")
 
 The default import `P(l, ...ls)` and `L.compose(l, ...ls)` both are the same as
@@ -559,6 +566,8 @@ L.set(L.findWith("x"), 3, [{z: 6}, {x: 9}, {y: 6}])
 
 #### [`L.firstOf(l, ...ls)`](#lfirstofl-ls "L.firstOf :: (PLens s a, ...PLens s a) -> PLens s a")
 
+**`L.firstOf` is deprecated and will be removed. See `L.choice` and `L.orElse`.**
+
 `L.firstOf(l, ...ls)` returns a partial lens that acts like the first of the
 given lenses, `l, ...ls`, whose view is not undefined on the given target.  When
 the views of all of the given lenses are undefined, the returned lens acts like
@@ -633,6 +642,25 @@ The main use case for `normalize` is to make it easy to determine whether, after
 a change, the data has actually changed.  By keeping the data normalized, a
 simple `R.equals` comparison will do.
 
+#### [`L.nothing`](#lnothing "L.nothing :: PLens s s")
+
+`L.nothing` is a special lens whose view is always undefined and setting through
+`L.nothing` has no effect.  In other words, for all `x` and `y`:
+
+```js
+  L.view(L.nothing, x) = undefined
+L.set(L.nothing, y, x) = x
+```
+
+`L.nothing` is the identity element of `L.choice`.
+
+#### [`L.orElse(backup, primary)`](#lorelsebackup-primary "L.orElse :: (PLens s a, PLens s a) -> PLens s a")
+
+`L.orElse(backup, primary)` acts like `primary` when its view is not undefined
+and otherwise like `backup`.  You can use `L.orElse` on its own with
+`R.reduceRight` (and `R.reduce`) to create an associative choice over lenses or
+use `L.orElse` to specify a default or backup lens for `L.choice`, for example.
+
 #### [`L.pick({p1: l1, ...pls})`](#lpickp1-l1-pls "L.pick :: {p1 :: PLens s a1, ...pls} -> PLens s {p1 :: a1, ...pls}")
 
 `L.pick({p1: l1, ...pls})` creates a lens out of the given object template of
diff --git a/package.json b/package.json
index 174c55f..f3ad73c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "partial.lenses",
-  "version": "2.0.0",
+  "version": "2.1.0",
   "description": "Ramda compatible partial lenses",
   "main": "lib/partial.lenses.js",
   "scripts": {
@@ -28,13 +28,13 @@
     "ramda": "^0.20.1"
   },
   "devDependencies": {
-    "babel-cli": "^6.6.5",
+    "babel-cli": "^6.7.5",
     "babel-eslint": "^6.0.2",
     "babel-plugin-transform-es2015-modules-commonjs": "^6.7.4",
     "babel-preset-es2015": "^6.6.0",
     "babel-preset-stage-2": "^6.5.0",
     "eslint": "^2.7.0",
     "mocha": "^2.4.5",
-    "nyc": "^6.1.1"
+    "nyc": "^6.2.1"
   }
 }
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index 5179f4a..0179b21 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -46,6 +46,10 @@ const toConserve = f => (y, c0) => conserve(f(y, c0), c0)
 
 //
 
+const snd = (_, c) => c
+
+//
+
 export const lift = l => {
   switch (typeof l) {
   case "string": return prop(l)
@@ -77,11 +81,18 @@ export const choose = x2yL => toFunctor => target => {
   return R.map(focus => R.set(l, focus, target), toFunctor(R.view(l, target)))
 }
 
-export const firstOf = (l, ...ls) => choose(x => {
-  const lls = [l, ...ls]
-  return lls[Math.max(0, lls.findIndex(l => view(l, x) !== undefined))]
+export const nothing = lens(snd, snd)
+
+export const orElse =
+  R.curry((d, l) => choose(x => view(l, x) !== undefined ? l : d))
+
+export const choice = (...ls) => choose(x => {
+  const i = ls.findIndex(l => view(l, x) !== undefined)
+  return 0 <= i ? ls[i] : nothing
 })
 
+export const firstOf = (l, ...ls) => orElse(l, choice(l, ...ls))
+
 export const replace = R.curry((inn, out) =>
   R.lens(x => R.equals(x, inn) ? out : x,
          toConserve(y => R.equals(y, out) ? inn : y)))
@@ -127,7 +138,7 @@ export const index = i => R.lens(xs => xs && xs[i], (x, xs) => {
   }
 })
 
-export const append = R.lens(() => {}, (x, xs) =>
+export const append = R.lens(snd, (x, xs) =>
   x === undefined ? xs : xs === undefined ? [x] : xs.concat([x]))
 
 export const filter = p => R.lens(xs => xs && xs.filter(p), (ys, xs) =>


Test patch:
diff --git a/test/tests.js b/test/tests.js
index b08029d..26bc892 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -27,6 +27,7 @@ describe("default === compose", () => {
 
 describe("arities", () => {
   testEq('L.augment.length', 1)
+  testEq('L.choice.length', 0)
   testEq('L.compose.length', 0)
   testEq('L.defaults.length', 1)
   testEq('L.define.length', 1)
@@ -36,6 +37,7 @@ describe("arities", () => {
   testEq('L.index.length', 1)
   testEq('L.lens.length', 2)
   testEq('L.normalize.length', 1)
+  testEq('L.orElse.length', 2)
   testEq('L.over.length', 3)
   testEq('L.pick.length', 1)
   testEq('L.prop.length', 1)
@@ -111,6 +113,27 @@ describe("L.normalize", () => {
          [1,3,5])
 })
 
+describe("L.nothing", () => {
+  testEq('L.view(L.nothing, "anything")', undefined)
+  testEq('L.set(L.nothing, "anything", "original")', "original")
+})
+
+describe("L.orElse", () => {
+  testEq('L.view(L.orElse("b", "a"), {a: 2, b: 1})', 2)
+  testEq('L.view(L.orElse("b", "a"), {b: 2})', 2)
+  testEq('L.set(L.orElse("b", "a"), 3, {a: 2, b: 1})', {a: 3, b: 1})
+  testEq('L.set(L.orElse("b", "a"), 3, {b: 2})', {b: 3})
+})
+
+describe("L.choice", () => {
+  testEq('L.view(L.choice("x", "y"), {x: "a"})', "a")
+  testEq('L.view(L.choice("x", "y"), {y: "b"})', "b")
+  testEq('L.view(L.choice("x", "y"), {z: "c"})', undefined)
+  testEq('L.set(L.choice("x", "y"), "A", {x: "a"})', {x: "A"})
+  testEq('L.set(L.choice("x", "y"), "B", {y: "b"})', {y: "B"})
+  testEq('L.set(L.choice("x", "y"), "C", {z: "c"})', {z: "c"})
+})
+
 describe("L.firstOf", () => {
   testEq('L.view(L.firstOf("x", "y"), {x: 11, y: 12})', 11)
   testEq('L.view(L.firstOf("y", "x"), {x: 11, y: 12})', 12)