Repository: calmm-js/partial.lenses
Pull request #18 (2016-04-06 17:42:26)

Working towards 3.0.0.
See #16 and #17 for some background.

polytypic: @raimohanska and @milankinen, if you have a moment to look through this pull request and comment it would be cool.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0bbdb2..a2152c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## 3.0.0
+
+Dropped *implicit* Ramda compatibility.  To interop with Ramda, one must now
+explicitly convert lenses using `L.toRamda` and `L.fromRamda`.  In particular,
+`L.compose` no longer necessarily returns a Ramda compatible lens and, in the
+future, the implementation may be changed more drastically.  This change was
+made, because now a lens returned by `L.compose` can take less memory and it
+will also be possible to further optimize the implementation in the future.
+
+Removed deprecated functions `L.view`, `L.over` and `L.firstOf`.
+
 ## 2.2.0
 
 Renamed `L.view` and `L.over`:
diff --git a/README.md b/README.md
index fa5cffc..e3c9a81 100644
--- a/README.md
+++ b/README.md
@@ -2,18 +2,16 @@
 
 Lenses are primarily a convenient abstraction for performing updates on
 individual elements of immutable data structures.  This library provides a
-collection of [Ramda](http://ramdajs.com/) compatible *partial* lenses.  While
-an ordinary lens can be used to view and update an existing part of a data
-structure, a partial lens can *view* optional data, *insert* new data, *update*
-existing data and *remove* existing data and can provide *defaults* and maintain
-*required* data structure parts.
+collection of *partial* lenses.  A partial lens can *view* optional data,
+*insert* new data, *update* existing data and *remove* existing data and can,
+for example, provide *defaults* and maintain *required* data structure parts.
 
 In JavaScript, missing data can be mapped to `undefined`, which is what partial
 lenses also do.  When a part of a data structure is missing, an attempt to view
 it returns `undefined`.  When a part is missing, setting it to a defined value
 inserts the new part.  Setting an existing part to `undefined` removes it.
-Partial lenses are defined in such a way that operations compose and one can
-conveniently and robustly operate on deeply nested data structures.
+Partial lenses are defined in such a way that operations [compose](#lcomposels)
+and one can conveniently and robustly operate on deeply nested data structures.
 
 [![npm version](https://badge.fury.io/js/partial.lenses.svg)](http://badge.fury.io/js/partial.lenses) [![Build Status](https://travis-ci.org/calmm-js/partial.lenses.svg?branch=master)](https://travis-ci.org/calmm-js/partial.lenses) [![](https://david-dm.org/calmm-js/partial.lenses.svg)](https://david-dm.org/calmm-js/partial.lenses) [![](https://david-dm.org/calmm-js/partial.lenses/dev-status.svg)](https://david-dm.org/calmm-js/partial.lenses#info=devDependencies) [![Gitter](https://img.shields.io/gitter/room/calmm-js/chat.js.svg?style=flat-square)](https://gitter.im/calmm-js/chat)
 
@@ -331,6 +329,9 @@ rather than `undefined`.
 
 ## Reference
 
+**Protip:** The link headings for functions in this reference have naive
+approximate types as tooltips.
+
 ### Usage
 
 The lenses and operations on lenses are accessed via the default import:
@@ -347,9 +348,7 @@ In alphabetical order.
 
 #### [`L.get(l, s)`](#lgetl-s "L.get :: PLens s a -> Maybe s -> Maybe a")
 
-`L.get(l, s)` is the same as `R.view(lift(l), s)` (see
-[view](http://ramdajs.com/0.20.0/docs/#view)) and returns the focused element
-from a data structure.
+`L.get(l, s)` returns the focused element from a data structure.
 
 For example:
 
@@ -360,9 +359,8 @@ L.get("y", {x: 112, y: 101})
 
 #### [`L.modify(l, x2x, s)`](#lmodifyl-x2x-s "L.modify :: PLens s a -> (Maybe a -> Maybe a) -> Maybe s -> Maybe s")
 
-`L.modify(l, x2x, s)` is the same as `R.over(lift(l), x2x, s)` (see
-[over](http://ramdajs.com/0.20.0/docs/#over)) and allows one to map over the
-focused element of a data structure.
+`L.modify(l, x2x, s)` allows one to map over the focused element of a data
+structure.
 
 For example:
 
@@ -371,10 +369,6 @@ L.modify("elems", R.map(L.remove("x")), {elems: [{x: 1, y: 2}, {x: 3, y: 4}]})
 // {elems: [{y: 2}, {y: 4}]}
 ```
 
-#### [`L.over(l, x2x, s)`](#loverl-x2x-s "L.over :: PLens s a -> (Maybe a -> Maybe a) -> Maybe s -> Maybe s")
-
-**`L.over` is a deprecated synonym for `L.modify` and will be removed.**
-
 #### [`L.remove(l, s)`](#lremovel-s "L.remove :: PLens s a -> Maybe s -> Maybe s")
 
 `L.remove(l, s)` is equivalent to `L.set(l, undefined, s)`.  With partial
@@ -404,11 +398,7 @@ L.removeAll(L.findWith("a"), [{x: 1}, {a: 2}, {a: 3, y: 4}, {z: 5}])
 
 #### [`L.set(l, x, s)`](#lsetl-x-s "L.set :: PLens s a -> Maybe a -> Maybe s -> Maybe s")
 
-`L.set(l, x, s)` is the same as `R.set(lift(l), x, s)` (see
-[set](http://ramdajs.com/0.20.0/docs/#set)) and is also equivalent to
-`L.modify(l, () => x, s)`.  Assuming that `0 <= i && i < xs.length` and `x !==
-undefined` then `L.set(i, x, xs)` is also equivalent to `R.update(i, x, xs)`
-(see [update](http://ramdajs.com/0.20.0/docs/#update)).
+`L.set(l, x, s)` is equivalent to `L.modify(l, () => x, s)`.
 
 For example:
 
@@ -417,10 +407,6 @@ L.set(P("a", 0, "x"), 11, {id: "z"})
 // {a: [{x: 11}], id: "z"}
 ```
 
-#### [`L.view(l, s)`](#lviewl-s "L.get :: PLens s a -> Maybe s -> Maybe a")
-
-**`L.view` is a deprecated synonym for `L.get` and will be removed.**
-
 ### Lens combinators
 
 In alphabetical order.
@@ -487,13 +473,18 @@ 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")
+#### [`L.compose(...ls)`](#lcomposels "L.compose :: (PLens s s1, ...PLens sN a) -> PLens s a")
+
+The default import `P` and `L.compose` refer to the one and same function, which
+performs lens composition.  The following equations characterize lens
+composition:
 
-The default import `P(l, ...ls)` and `L.compose(l, ...ls)` both are the same as
-`R.compose(lift(l), ...ls.map(lift))` (see
-[compose](http://ramdajs.com/0.20.0/docs/#compose)) and compose a lens from a
-path of lenses.  Furthermore, `L.compose()` is the same as `L.identity`, which
-reflects the fact that `L.identity` is the identity element of lens composition.
+```js
+                  L.compose() = L.identity
+                 L.compose(l) = l
+   L.get(L.compose(l, ...ls)) = R.pipe(L.get(l), ...ls.map(L.get))
+L.modify(L.compose(l, ...ls)) = R.pipe(L.modify(l), ...ls.map(L.modify))
+```
 
 For example:
 
@@ -557,11 +548,11 @@ L.removeAll(L.find(x => x <= 2), [3,1,4,1,5,9,2])
 // [ 3, 4, 5, 9 ]
 ```
 
-#### [`L.findWith(l, ...ls)`](#lfindwithl-ls "L.findWith :: (PLens s s1, ...PLens sN a) -> PLens [s] a")
+#### [`L.findWith(...ls)`](#lfindwithls "L.findWith :: (PLens s s1, ...PLens sN a) -> PLens [s] a")
 
-`L.findWith(l, ...ls)` chooses an index from an array through which the given
-lens, `P(l, ...ls)`, focuses on a defined item and then returns a lens that
-focuses on that item.
+`L.findWith(...ls)` chooses an index from an array through which the given lens,
+`P(...ls)`, focuses on a defined item and then returns a lens that focuses on
+that item.
 
 For example:
 
@@ -572,35 +563,27 @@ L.set(L.findWith("x"), 3, [{z: 6}, {x: 9}, {y: 6}])
 // [ { z: 6 }, { x: 3 }, { 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
-`l`.
-
-Note that `L.firstOf` is an associative operation, but there is no identity
-element.
-
 #### [`L.identity`](#lidentity "L.identity :: PLens s s")
 
-`L.identity` is equivalent to `R.lens(R.identity, R.identity)` and is the
-identity element of lenses: both `P(L.identity, l)` and `P(l, L.identity)` are
-equivalent to `l`.
+`L.identity` is the identity element of lens composition.  The following
+equations characterize `L.identity`:
+
+```js
+      L.get(L.identity, x) = x
+L.modify(L.identity, f, x) = f(x)
+  L.compose(L.identity, l) = l
+  L.compose(l, L.identity) = l
+```
 
 #### [`L.index(integer)`](#lindexinteger "L.index :: Integer -> PLens [a] a")
 
-`L.index(integer)` or `P(integer)` is similar to `R.lensIndex(integer)` (see
-[lensIndex](http://ramdajs.com/0.20.0/docs/#lensIndex)), but acts as a partial
-lens:
+`L.index(integer)` or `integer` focuses on the specified array index.
+
 * When viewing an undefined array index or an undefined array, the result is
   undefined.
-* When setting an array index to undefined, the element is removed from the
-  resulting array, shifting all higher indices down by one.  If the result would
-  be an array without indices (ignoring length), the whole result will be
-  undefined.
+* When setting to undefined, the element is removed from the resulting array,
+  shifting all higher indices down by one.  If the result would be an array
+  without indices (ignoring length), the whole result will be undefined.
 
 **NOTE:** There is a gotcha related to removing elements from an array.  Namely,
 when the last element is removed, the result is `undefined` rather than an empty
@@ -632,8 +615,40 @@ L.remove(P("elems", L.required([]), 0), {elems: ["b"], some: "thing"})
 
 #### [`L.lens(get, set)`](#llensget-set "L.lens :: (Maybe s -> Maybe a) -> (Maybe a -> Maybe s -> Maybe s) -> PLens s a")
 
-`L.lens(get, set)` is the same as `R.lens(get, set)` (see
-[lens](http://ramdajs.com/0.20.0/docs/#lens)) and creates a new primitive lens.
+`L.lens(get, set)` creates a new primitive lens.  One should think twice before
+introducing a new primitive lens&mdash;most of the combinators in this library
+have been introduced to reduce the need to write new primitive lenses.  With
+that said, there are still valid reasons to create new primitive lenses.  For
+example, here is a lens that we've used in production, written with the help of
+[Moment.js](http://momentjs.com/), to bidirectionally convert a pair of `start`
+and `end` times to a duration:
+
+```js
+const timesAsDuration = L.lens(
+  ({start, end} = {}) => {
+    if (undefined === start)
+      return undefined
+    if (undefined === end)
+      return "Infinity"
+    return moment.duration(moment(end).diff(moment(start))).toJSON()
+  },
+  (duration, {start = moment().toJSON()} = {}) => {
+    if (undefined === duration || "Infinity" === duration) {
+      return {start}
+    } else {
+      return {
+        start,
+        end: moment(start).add(moment.duration(duration)).toJSON()
+      };
+    }
+  }
+)
+```
+
+When composed with `L.pick`, to flexibly pick the `start` and `end` times, the
+above can be adapted to work in a wide variety of cases.  However, the above
+lens will never be added to this library, because it would require adding
+dependency to [Moment.js](http://momentjs.com/).
 
 #### [`L.normalize(value => value)`](#lnormalizevalue--value "L.normalize :: (s -> s) -> PLens s s")
 
@@ -720,15 +735,14 @@ be an object.
 
 #### [`L.prop(string)`](#lpropstring "L.prop :: (p :: a) -> PLens {p :: a, ...ps} a")
 
-`L.prop(string)` or `P(string)` is similar to `R.lensProp(string)` (see
-[lensProp](http://ramdajs.com/0.20.0/docs/#lensProp)), but acts as a partial
-lens:
+`L.prop(string)` or `string` focuses on the specified object property.
+
 * When viewing an undefined property or an undefined object, the result is
   undefined.
 * When setting property to undefined, the property is removed from the result.
   If the result would be an empty object, the whole result will be undefined.
 
-#### [`L.props(key, ...keys)`](#lpropskey-keys "L.props :: (p1 :: a1, ...ps) -> PLens {p1 :: a1, ...ps, ...o} {p1 :: a1, ...ps}")
+#### [`L.props(...strings)`](#lpropsstrings "L.props :: (p1 :: a1, ...ps) -> PLens {p1 :: a1, ...ps, ...o} {p1 :: a1, ...ps}")
 
 `L.props(k1, ..., kN)` is equivalent to `L.pick({[k1]: k1, ..., [kN]: kN})` and
 focuses on a subset of properties of an object, allowing one to treat the subset
@@ -779,24 +793,19 @@ L.remove(P("items", L.required([]), 0), {items: [1]})
 // { items: [] }
 ```
 
-### Auxiliary
+### Interop
 
-#### [`L.lift(pl)`](#lliftpl "L.lift :: (p :: a) -> PLens {p :: a, ...ps} a & Integer -> PLens [a] a & PLens s a -> PLens s a")
+Conversions between lens libraries.
 
-The idempotent `lift` operation is defined as
+#### [`L.fromRamda(lens)`](#lfromramdalens "L.fromRamda :: Lens s a -> PLens s a")
 
-```js
-const lift = l => {
-  switch (typeof l) {
-  case "string": return L.prop(l)
-  case "number": return L.index(l)
-  default:       return l
-  }
-}
-```
+`L.fromRamda(lens)` converts the given Ramda lens to a partial lens.  Note that
+this does not change the behavior of the lens on undefined values.
+
+#### [`L.toRamda(plens)`](#ltoramdaplens "L.toRamda :: PLens s a -> Lens s a")
 
-and is available as a non-default export.  All operations in this library that
-take lenses as arguments implicitly lift them.
+`L.toRamda(plens)` converts the given partial lens to a Ramda lens.  Note that
+this does not change the behavior of the lens on undefined values.
 
 ## Background
 
@@ -823,7 +832,7 @@ One might assume that `R.lensPath([p0, ...ps])` is equivalent to
 `R.compose(R.lensProp(p0), ...ps.map(R.lensProp))`, but that is not the case.
 
 With partial lenses you can robustly compose a path lens from prop lenses
-`R.compose(L.prop(p0), ...ps.map(L.prop))` or just use the shorthand notation
+`L.compose(L.prop(p0), ...ps.map(L.prop))` or just use the shorthand notation
 `P(p0, ...ps)`.
 
 ### Types
diff --git a/package.json b/package.json
index 6687888..3d98ba6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "partial.lenses",
-  "version": "2.2.1",
-  "description": "Ramda compatible partial lenses",
+  "version": "3.0.0",
+  "description": "Partial lenses",
   "main": "lib/partial.lenses.js",
   "scripts": {
     "dist": "babel src --source-maps inline --out-dir lib",
@@ -14,10 +14,10 @@
     "url": "git+https://github.com/calmm-js/partial.lenses.git"
   },
   "keywords": [
-    "ramda",
+    "json",
     "lens",
     "partial",
-    "json"
+    "ramda"
   ],
   "license": "MIT",
   "bugs": {
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index ef46294..3e90e52 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -1,16 +1,21 @@
-import R from "ramda"
+import * as R from "ramda"
 
 //
 
-const warned = {}
+const id = x => x
+const snd = (_, c) => c
 
-const deprecated = message => {
-  if (!(message in warned)) {
-    warned[message] = message
-    console.warn("partial.lenses:", message)
-  }
+//
+
+const check = (expected, predicate) => x => {
+  if (predicate(x))
+    return x
+  else
+    throw new Error(`Expected ${expected}, but got ${x}.`)
 }
 
+const assert = process.env.NODE_ENV === "production" ? () => id : check
+
 //
 
 const empty = {}
@@ -57,24 +62,22 @@ const toConserve = f => (y, c0) => conserve(f(y, c0), c0)
 
 //
 
-const snd = (_, c) => c
+const seemsLens = x => typeof x === "function" && x.length === 1
 
-//
+export const fromRamda = assert("a lens", seemsLens)
 
-export const lift = l => {
-  switch (typeof l) {
-  case "string": return prop(l)
-  case "number": return index(l)
-  default:       return l
-  }
+export const toRamda = l => {
+  if (isProp(l))  return toRamdaProp(l)
+  if (isIndex(l)) return toRamdaIndex(l)
+  return fromRamda(l)
 }
 
 export const compose = (...ls) =>
-  ls.length === 0 ? identity    :
-  ls.length === 1 ? lift(ls[0]) :
-  R.compose(...ls.map(lift))
+  ls.length === 0 ? identity :
+  ls.length === 1 ? ls[0] :
+  R.compose(...ls.map(toRamda))
 
-export const remove = R.curry((l, s) => R.set(lift(l), undefined, s))
+export const remove = R.curry((l, s) => R.set(toRamda(l), undefined, s))
 
 export const removeAll = R.curry((lens, data) => {
   while (get(lens, data) !== undefined)
@@ -83,18 +86,12 @@ export const removeAll = R.curry((lens, data) => {
 })
 
 export const lens = R.lens
-export const modify = R.curry((l, x2x, s) => R.over(lift(l), x2x, s))
-export const over = R.curry((l, x2x, s) =>
-  deprecated("`over` has been deprecated --- use `modify`") ||
-  R.over(lift(l), x2x, s))
-export const set = R.curry((l, x, s) => R.set(lift(l), x, s))
-export const get = R.curry((l, s) => R.view(lift(l), s))
-export const view = R.curry((l, s) =>
-  deprecated("`view` has been deprecated --- use `get`") ||
-  R.view(lift(l), s))
+export const modify = R.curry((l, x2x, s) => R.over(toRamda(l), x2x, s))
+export const set = R.curry((l, x, s) => R.set(toRamda(l), x, s))
+export const get = R.curry((l, s) => R.view(toRamda(l), s))
 
 export const choose = x2yL => toFunctor => target => {
-  const l = lift(x2yL(target))
+  const l = toRamda(x2yL(target))
   return R.map(focus => R.set(l, focus, target), toFunctor(R.view(l, target)))
 }
 
@@ -108,24 +105,24 @@ export const choice = (...ls) => choose(x => {
   return 0 <= i ? ls[i] : nothing
 })
 
-export const firstOf = (l, ...ls) =>
-  deprecated("`firstOf` has been deprecated --- use `choice` and `orElse`") ||
-  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)))
+  lens(x => R.equals(x, inn) ? out : x,
+       toConserve(y => R.equals(y, out) ? inn : y)))
 
 export const defaults = replace(undefined)
 export const required = inn => replace(inn, undefined)
 export const define = v => R.compose(required(v), defaults(v))
 
 export const normalize = transform =>
-  R.lens(toPartial(transform), toConserve(toPartial(transform)))
+  lens(toPartial(transform), toConserve(toPartial(transform)))
+
+const isProp = x => typeof x === "string"
+
+export const prop = assert("a string", isProp)
 
-export const prop = k =>
-  R.lens(o => o && o[k],
-         (v, o) => v === undefined ? deleteKey(k, o) : setKey(k, v, o))
+const toRamdaProp = k =>
+  lens(o => o && o[k],
+       (v, o) => v === undefined ? deleteKey(k, o) : setKey(k, v, o))
 
 export const find = predicate => choose(xs => {
   if (xs === undefined)
@@ -134,12 +131,16 @@ export const find = predicate => choose(xs => {
   return i < 0 ? append : i
 })
 
-export const findWith = (l, ...ls) => {
-  const lls = compose(l, ...ls)
+export const findWith = (...ls) => {
+  const lls = toRamda(compose(...ls))
   return compose(find(x => R.view(lls, x) !== undefined), lls)
 }
 
-export const index = i => R.lens(xs => xs && xs[i], (x, xs) => {
+const isIndex = x => Number.isInteger(x) && 0 <= x
+
+export const index = assert("a non-negative integer", isIndex)
+
+const toRamdaIndex = i => lens(xs => xs && xs[i], (x, xs) => {
   if (x === undefined) {
     if (xs === undefined)
       return undefined
@@ -157,13 +158,13 @@ export const index = i => R.lens(xs => xs && xs[i], (x, xs) => {
   }
 })
 
-export const append = R.lens(snd, (x, xs) =>
+export const append = 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) =>
+export const filter = p => lens(xs => xs && xs.filter(p), (ys, xs) =>
   conserve(dropped(R.concat(ys || [], (xs || []).filter(R.complement(p)))), xs))
 
-export const augment = template => R.lens(
+export const augment = template => lens(
   toPartial(x => {
     const z = {...x}
     for (const k in template)
@@ -189,7 +190,7 @@ export const augment = template => R.lens(
     return z
   }))
 
-export const pick = template => R.lens(
+export const pick = template => lens(
   c => {
     let r
     for (const k in template) {
@@ -209,11 +210,8 @@ export const pick = template => R.lens(
     return c
   })
 
-export const identity = R.lens(R.identity, conserve)
+export const identity = lens(id, conserve)
 
-export const props = (k, ...ks) => {
-  const kks = [k, ...ks]
-  return pick(R.zipObj(kks, kks))
-}
+export const props = (...ks) => pick(R.zipObj(ks, ks))
 
 export default compose


Test patch:
diff --git a/test/tests.js b/test/tests.js
index 2d407b7..e3148e0 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -1,4 +1,4 @@
-import R from "ramda"
+import * as R from "ramda"
 
 import P, * as L from "../src/partial.lenses"
 
@@ -12,17 +12,33 @@ function show(x) {
   }
 }
 
+const run = expr => eval(`(P, L, R) => ${expr}`)(P, L, R)
+
 const testEq = (expr, expect) => it(`${expr} => ${show(expect)}`, () => {
-  const actual = eval(`(P, L, R) => ${expr}`)(P, L, R)
+  const actual = run(expr)
   if (!R.equals(actual, expect))
     throw new Error(`Expected: ${show(expect)}, actual: ${show(actual)}`)
 })
 
-describe("default === compose", () => {
-  it("P === L.compose", () => {
-     if (P !== L.compose)
-       throw new Error("Not the same")
-  })
+const testThrows = expr => it(`${expr} => throws`, () => {
+  let raised
+  let result
+  try {
+    result = run(expr)
+    raised = false
+  } catch (e) {
+    result = e
+    raised = true
+  }
+  if (!raised)
+    throw new Error(`Expected ${expr} to throw, returned ${show(result)}`)
+})
+
+describe("compose", () => {
+  testEq("P === L.compose", true)
+  testEq('P() === L.identity', true)
+  testEq('P("x")', "x")
+  testEq('P(101)', 101)
 })
 
 describe("arities", () => {
@@ -33,7 +49,8 @@ describe("arities", () => {
   testEq('L.define.length', 1)
   testEq('L.filter.length', 1)
   testEq('L.find.length', 1)
-  testEq('L.findWith.length', 1)
+  testEq('L.findWith.length', 0)
+  testEq('L.fromRamda.length', 1)
   testEq('L.get.length', 2)
   testEq('L.index.length', 1)
   testEq('L.lens.length', 2)
@@ -42,11 +59,17 @@ describe("arities", () => {
   testEq('L.orElse.length', 2)
   testEq('L.pick.length', 1)
   testEq('L.prop.length', 1)
-  testEq('L.props.length', 1)
+  testEq('L.props.length', 0)
   testEq('L.remove.length', 2)
   testEq('L.replace.length', 2)
   testEq('L.required.length', 1)
   testEq('L.set.length', 3)
+  testEq('L.toRamda.length', 1)
+})
+
+describe("interop", () => {
+  testEq('R.set(L.toRamda(0), "a", ["b"])', ["a"])
+  testEq('L.get(L.fromRamda(R.lensProp("x")), {x: "b"})', "b")
 })
 
 describe('L.find', () => {
@@ -61,6 +84,11 @@ describe('L.find', () => {
 })
 
 describe('L.index', () => {
+  if (process.env.NODE_ENV !== "production") {
+    testThrows('L.index("x")')
+    testThrows('L.index(-1)')
+    testThrows('L.index()')
+  }
   testEq('L.set(P(1), undefined, [,,])', undefined)
   testEq('L.set(P(L.required([]), 1), undefined, [,,])', [])
   testEq('L.set(P(1), 4, [1, 2, 3])', [1, 4, 3])
@@ -76,6 +104,11 @@ describe('L.index', () => {
 })
 
 describe('L.prop', () => {
+  if (process.env.NODE_ENV !== "production") {
+    testThrows('L.prop(2)')
+    testThrows('L.prop(x => x)')
+    testThrows('L.prop()')
+  }
   testEq('L.set(P("x"), undefined, {x: 1})', undefined)
   testEq('L.set(P("x", L.required(null)), undefined, {x: 1})', {x: null})
   testEq('L.set(P("x", L.required(null)), 2, {x: 1})', {x: 2})
@@ -134,19 +167,6 @@ describe("L.choice", () => {
   testEq('L.set(L.choice("x", "y"), "C", {z: "c"})', {z: "c"})
 })
 
-describe("L.firstOf", () => {
-  testEq('L.get(L.firstOf("x", "y"), {x: 11, y: 12})', 11)
-  testEq('L.get(L.firstOf("y", "x"), {x: 11, y: 12})', 12)
-  testEq('L.get(L.firstOf("x", "y"), {z: 13})', undefined)
-  testEq('L.modify(L.firstOf("x", "y"), x => x-2, {x: 11, y: 12})', {x: 9, y: 12})
-  testEq('L.modify(L.firstOf("y", "x"), x => x-2, {x: 11, y: 12})', {x: 11, y: 10})
-  testEq('L.set(L.firstOf("x", "y"), 12, {z: 13})', {x: 12, z: 13})
-  testEq('L.set(L.firstOf("y", "x"), 12, {z: 13})', {y: 12, z: 13})
-  testEq('L.remove(L.firstOf("x", "y"), {z: 13})', {z: 13})
-  testEq('L.remove(L.firstOf("x", "y"), {x: 11, y: 12})', {y: 12})
-  testEq('L.remove(L.firstOf("y", "x"), {x: 11, y: 12})', {x: 11})
-})
-
 describe("L.findWith", () => {
   testEq('L.get(L.findWith("x", 1), [{x: ["a"]},{x: ["b","c"]}])', "c")
   testEq('L.set(L.findWith("x", 1), "d", [{x: ["a"]},{x: ["b","c"]}])', [{x: ["a"]},{x: ["b","d"]}])
@@ -183,7 +203,7 @@ describe("L.pick", () => {
   testEq('L.set(P(L.pick({x: "b", y: "a"}), "x"), 3, {a: [2], b: 1})', {a: [2], b: 3})
   testEq('L.remove(P(L.pick({x: "b", y: "a"}), "y"), {a: [2], b: 1})', {b: 1})
   testEq('L.remove(P(L.pick({x: "b"}), "x"), {a: [2], b: 1})', {a: [2]})
-  testEq('L.removeAll(P(L.pick({x: "b", y: "a"}), L.firstOf("y", "x")), {a: [2], b: 1})', undefined)
+  testEq('L.removeAll(P(L.pick({x: "b", y: "a"}), L.choice("y", "x")), {a: [2], b: 1})', undefined)
 })
 
 describe("L.props", () => {