Repository: calmm-js/partial.lenses (js)

Issue:
Object.freeze
Currently optics do not [freeze](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) the new objects and arrays they create, but it would not be difficult to make it so.

Personally I have not found it to be a problem that the resulting objects are mutable (I just don't mutate them), but I can see that in some contexts it could be helpful to have the result objects frozen.

A problem with `Object.freeze` is that it can be expensive in some JS engines.  So, I would not use it in production builds unless there was a really good reason to do so.

So, one approach might be to have an additional opt-in configuration environment variable that can be explicitly defined, like `PARTIAL_LENSES__FREEZE=1`, which results in a build of the library that freezes any new objects returned by optics.  However, by default, even in non-production builds, new objects would not be frozen.


Pull request description:
WIP Freezing
closes #50

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c374193..938a161 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
 
 ## 10.0.0
 
+As discussed in
+issue [50](https://github.com/calmm-js/partial.lenses/issues/50), to strongly
+guide away from mutating data structures, optics
+now
+[`Object.freeze`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) any
+new objects they create when `NODE_ENV` is not `production`.  Note that optics
+do not implicitly "deep freeze" data structures given to them or freeze data
+returned by user defined functions.  Only objects newly created by optic
+functions themselves are frozen.
+
 Removed previously obsoleted exports:
 
 * `L.firstAs`,
diff --git a/README.md b/README.md
index a894b4e..7d4ceb6 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
 # <a id="partial-lenses"></a> Partial Lenses &middot; [![Gitter](https://img.shields.io/gitter/room/calmm-js/chat.js.svg)](https://gitter.im/calmm-js/chat) [![GitHub stars](https://img.shields.io/github/stars/calmm-js/partial.lenses.svg?style=social)](https://github.com/calmm-js/partial.lenses) [![npm](https://img.shields.io/npm/dm/partial.lenses.svg)](https://www.npmjs.com/package/partial.lenses)
 
 Lenses are basically an abstraction for simultaneously specifying operations
-to [update](#L-modify) and [query](#L-get) immutable data structures.  Lenses
-are [highly composable](#on-composability) and can be [efficient](#benchmarks).
-This library provides a collection
+to [update](#L-modify) and [query](#L-get) [immutable](#on-immutability) data
+structures.  Lenses are [highly composable](#on-composability) and can
+be [efficient](#benchmarks).  This library provides a collection
 of [partial](#on-partiality) [isomorphisms](#isomorphisms), [lenses](#lenses),
 and [traversals](#traversals), collectively known as [optics](#optics), for
 manipulating [JSON](http://json.org/) and
@@ -27,6 +27,7 @@ parts.  [Try Lenses!](https://calmm-js.github.io/partial.lenses/playground.html)
 * [Reference](#reference)
   * [Optics](#optics)
     * [On partiality](#on-partiality)
+    * [On immutability](#on-immutability)
     * [On composability](#on-composability)
     * [Operations on optics](#operations-on-optics)
       * [`L.modify(optic, (maybeValue, index) => maybeValue, maybeData) ~> maybeData`](#L-modify "L.modify: POptic s a -> ((Maybe a, Index) -> Maybe a) -> Maybe s -> Maybe s")
@@ -159,10 +160,10 @@ language.  Given a language, we want to be able to
 * remove an existing text and the immediately surrounding object.
 
 Furthermore, when updating, inserting, and removing texts, we'd like the
-operations to treat the JSON as immutable and create new JSON objects with the
-changes rather than mutate existing JSON objects, because this makes it trivial
-to support features such as undo-redo and can also help to avoid bugs associated
-with mutable state.
+operations to treat the JSON as [immutable](#on-immutability) and create new
+JSON objects with the changes rather than mutate existing JSON objects, because
+this makes it trivial to support features such as undo-redo and can also help to
+avoid bugs associated with mutable state.
 
 Operations like these are what lenses are good at.  Lenses can be seen as a
 simple embedded [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
@@ -588,6 +589,22 @@ and removal.  It also allows to reduce the number of necessary abstractions and
 it tends to make compositions of optics more concise with fewer required parts,
 which both help to avoid bugs.
 
+#### On immutability
+
+Starting with version [10.0.0](CHANGELOG.md#1000), to strongly guide away from
+mutating data structures, optics
+call
+[`Object.freeze`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) on
+any new objects they create when `NODE_ENV` is not `production`.
+
+Why only non-`production` builds?  Because `Object.freeze` can be quite
+expensive and the main benefit is in catching potential bugs early during
+development.
+
+Also note that optics do not implicitly "deep freeze" data structures given to
+them or freeze data returned by user defined functions.  Only objects newly
+created by optic functions themselves are frozen.
+
 #### On composability
 
 A lot of libraries these days claim to
@@ -2665,7 +2682,8 @@ that signal errors later than when using total optics.
 JSON is the data-interchange format of choice today.  By being able to
 effectively and efficiently manipulate JSON data structures directly, one can
 avoid using special internal representations of data and make things simpler
-(e.g. no need to convert from JSON to efficient immutable collections and back).
+(e.g. no need to convert from JSON to efficient [immutable](#on-immutability)
+collections and back).
 
 #### Use of `undefined`
 
@@ -2886,8 +2904,8 @@ See [bench.js](./bench/bench.js) for details.
 ### Lenses all the way
 
 As said in the first sentence of this document, lenses are convenient for
-performing updates on individual elements of immutable data structures.  Having
-abilities such
+performing updates on individual elements of [immutable](#on-immutability) data
+structures.  Having abilities such
 as [nesting](#L-compose), [adapting](#L-choose), [recursing](#L-lazy)
 and [restructuring](#L-pick) using lenses makes the notion of an individual
 element quite flexible and, even further, [traversals](#traversals) make it
diff --git a/src/partial.lenses.js b/src/partial.lenses.js
index fe2d104..542605f 100644
--- a/src/partial.lenses.js
+++ b/src/partial.lenses.js
@@ -3,6 +3,7 @@ import {
   always,
   applyU,
   arityN,
+  array0,
   assocPartialU,
   constructorOf,
   curry,
@@ -49,6 +50,7 @@ function mapPartialIndexU(xi2y, xs) {
   if (j) {
     if (j < n)
       ys.length = j
+    if (process.env.NODE_ENV !== "production") Object.freeze(ys)
     return ys
   }
 }
@@ -56,6 +58,8 @@ function mapPartialIndexU(xi2y, xs) {
 function copyToFrom(ys, k, xs, i, j) {
   while (i < j)
     ys[k++] = xs[i++]
+  if (process.env.NODE_ENV !== "production" && ys.length === k)
+    Object.freeze(ys)
   return ys
 }
 
@@ -150,6 +154,7 @@ function toArray(n) {
   if (void 0 !== n) {
     const ys = []
     pushTo(n, ys)
+    if (process.env.NODE_ENV !== "production") Object.freeze(ys)
     return ys
   }
 }
@@ -230,8 +235,10 @@ const lensFrom = (get, set) => i => (F, xi2yF, x, _) =>
 
 const getProp = (k, o) => o instanceof Object ? o[k] : void 0
 
-const setProp = (k, v, o) =>
-  void 0 !== v ? assocPartialU(k, v, o) : dissocPartialU(k, o)
+function setProp(k, v, o) {
+  const r = void 0 !== v ? assocPartialU(k, v, o) : dissocPartialU(k, o)
+  return process.env.NODE_ENV !== "production" && r ? Object.freeze(r) : r
+}
 
 const funProp = lensFrom(getProp, setProp)
 
@@ -250,6 +257,7 @@ function setIndex(i, x, xs) {
     for (let j=0; j<m; ++j)
       ys[j] = xs[j]
     ys[i] = x
+    if (process.env.NODE_ENV !== "production") Object.freeze(ys)
     return ys
   } else {
     if (0 < n) {
@@ -261,6 +269,7 @@ function setIndex(i, x, xs) {
           ys[j] = xs[j]
         for (let j=i+1; j<n; ++j)
           ys[j-1] = xs[j]
+        if (process.env.NODE_ENV !== "production") Object.freeze(ys)
         return ys
       }
     }
@@ -372,6 +381,7 @@ function getPick(template, x) {
       r[k] = v
     }
   }
+  if (process.env.NODE_ENV !== "production" && r) Object.freeze(r)
   return r
 }
 
@@ -416,6 +426,7 @@ const branchOnMerge = (x, keys) => xs => {
       r[k] = v
     }
   }
+  if (process.env.NODE_ENV !== "production" && r) Object.freeze(r)
   return r
 }
 
@@ -463,6 +474,10 @@ function findIndex(xi2b, xs) {
 function partitionIntoIndex(xi2b, xs, ts, fs) {
   for (let i=0, n=xs.length, x; i<n; ++i)
     (xi2b(x = xs[i], i) ? ts : fs).push(x)
+  if (process.env.NODE_ENV !== "production") {
+    Object.freeze(ts)
+    Object.freeze(fs)
+  }
 }
 
 const fromReader = wi2x => (F, xi2yF, w, i) =>
@@ -602,7 +617,7 @@ export const and = all(id)
 export const any = pipe2U(mkSelect(x => x ? T : U), Boolean)
 
 export const collectAs = curry((xi2y, t, s) =>
-  toArray(run(t, Collect, xi2y, s)) || [])
+  toArray(run(t, Collect, xi2y, s)) || array0)
 
 export const collect = collectAs(id)
 
@@ -691,6 +706,7 @@ export function augment(template) {
       if (x)
         for (const k in template)
           x[k] = template[k](x)
+      if (process.env.NODE_ENV !== "production" && x) Object.freeze(x)
       return x
     },
     (y, x) => {
@@ -713,6 +729,7 @@ export function augment(template) {
           if (x && hasU(k, x))
             set(k, x[k])
       }
+      if (process.env.NODE_ENV !== "production" && z) Object.freeze(z)
       return z
     })
 }


Test patch:
diff --git a/test/core.js b/test/core.js
index 7fa2294..fb373e9 100644
--- a/test/core.js
+++ b/test/core.js
@@ -14,8 +14,9 @@ const fromArrayLike = xs => R.map(i => xs[i], R.range(0, xs.length))
 
 const isDefined = x => x !== undefined
 
-const Collect = {empty: R.always([]), concat: (x, y) => x.concat(y)}
-const toCollect = x => isDefined(x) ? [x] : []
+const Collect = {empty: R.always(Object.freeze([])),
+                 concat: (x, y) => Object.freeze(x.concat(y))}
+const toCollect = x => Object.freeze(isDefined(x) ? [x] : [])
 
 const maxPartial = (x, y) => isDefined(x) && (!isDefined(y) || x > y) ? x : y
 const minPartial = (x, y) => isDefined(x) && (!isDefined(y) || x < y) ? x : y
@@ -137,13 +138,13 @@ export const rewrite = xi2x =>
 export const append = choose(s => seemsArrayLike(s) ? s.length : 0)
 export const filter = p => lens(
   xs => seemsArrayLike(xs)
-    ? fromArrayLike(xs).filter((x, i) => p(x, i))
+    ? Object.freeze(fromArrayLike(xs).filter((x, i) => p(x, i)))
     : undefined,
   (ys, xs) => {
     const zs = [].concat(
       fromArrayLike(ys || []),
       seemsArrayLike(xs) ? fromArrayLike(xs).filter((x, i) => !p(x, i)) : [])
-    return zs.length ? zs : undefined
+    return zs.length ? Object.freeze(zs) : undefined
   })
 export const find = p => choose(xs => {
   if (!seemsArrayLike(xs))
@@ -158,7 +159,9 @@ export const findWith = (...ls) => {
 export const index = L.index
 export const last = L.last
 export const slice = R.curry((b, e) => lens(
-  xs => seemsArrayLike(xs) ? fromArrayLike(xs).slice(b, e) : undefined,
+  xs => seemsArrayLike(xs)
+    ? Object.freeze(fromArrayLike(xs).slice(b, e))
+    : undefined,
   (ys, xs) => {
     xs = seemsArrayLike(xs) ? fromArrayLike(xs) : []
     const i = clamp(0, xs.length,
@@ -171,7 +174,7 @@ export const slice = R.curry((b, e) => lens(
                                         undefined === e ? xs.length :
                                         e < 0 ? xs.length + e :
                                         e)))
-    return zs.length ? zs : undefined
+    return zs.length ? Object.freeze(zs) : undefined
   }
 ))
 
diff --git a/test/tests.js b/test/tests.js
index 6c0c45d..525651b 100644
--- a/test/tests.js
+++ b/test/tests.js
@@ -48,17 +48,17 @@ const everywhere = [L.optional, L.lazy(rec => {
 })]
 
 const CollectM = {
-  of: x => [x, []],
+  of: x => [x, Object.freeze([])],
   map: (x2y, [x, s]) => [x2y(x), s],
-  ap: ([x2y, sl], [x, sr]) => [x2y(x), [...sr, ...sl]],
+  ap: ([x2y, sl], [x, sr]) => [x2y(x), Object.freeze([...sr, ...sl])],
   chain: (x2yM, [x, sr]) => {
     const [y, sl] = x2yM(x)
-    return [y, [...sr, ...sl]]
+    return [y, Object.freeze([...sr, ...sl])]
   }
 }
 
 const collectM = (o, s) =>
-  L.toFunction(o)(CollectM, x => [x, [x]], s, undefined)[1]
+  L.toFunction(o)(CollectM, x => [x, [x]], Object.freeze(s), undefined)[1]
 
 //
 
diff --git a/test/type.js b/test/type.js
index d498a43..31abcfe 100644
--- a/test/type.js
+++ b/test/type.js
@@ -12,7 +12,7 @@ export const lazy = ty2ty => {
 
 export const any = x => x
 
-export const and = R.compose
+export const and = R.pipe
 
 export const or = (...ps) => x => {
   const es = [], n = ps.length
@@ -35,6 +35,28 @@ export const fromPredicate = p => x => {
 const type = t => fromPredicate(x => typeof x === t)
 const instance = c => fromPredicate(x => x instanceof c)
 
+const isntFreezable = x =>
+  x === null
+  || typeof x !== "object"
+  || x instanceof Int8Array
+
+const isFrozen = x =>
+  isntFreezable(x)
+  || Object.isFrozen(x)
+
+const isDeepFrozen = x =>
+  isntFreezable(x)
+  || Object.isFrozen(x)
+     && !Object.getOwnPropertyNames(x).find(x => !isDeepFrozen(x))
+
+export const frozen = fromPredicate(isFrozen)
+
+export const deepFrozen = fromPredicate(isDeepFrozen)
+
+export const deepFreeze = x => isFrozen(x) ? x
+  : (Object.getOwnPropertyNames(x).forEach(k => deepFreeze(x[k])),
+     Object.freeze(x))
+
 export const integer = fromPredicate(Number.isInteger)
 export const nonNegative = and(integer, fromPredicate(x => 0 <= x))
 
diff --git a/test/types.js b/test/types.js
index bc43d24..cedb97b 100644
--- a/test/types.js
+++ b/test/types.js
@@ -1,7 +1,14 @@
 import * as T from "./type"
 
-const T_maybeData = T.any
-const T_data = T.def
+const T_deepFrozenOnDev =
+  process.env.NODE_ENV === "production" ? T.any : T.deepFrozen
+
+const T_maybeDataI = T.deepFreeze
+const T_maybeDataO = T_deepFrozenOnDev
+
+const T_dataI = T.and(T.def, T.deepFreeze)
+const T_dataO = T.and(T.def, T_deepFrozenOnDev)
+
 const T_index = T.or(T.nonNegative, T.string, T.undef)
 const T_sliceIndex = T.or(T.integer, T.undef)
 
@@ -29,16 +36,16 @@ const T_monad = T.object({
 
 const T_opticFnOf = Category =>
   T.fn([Category,
-        T.fn([T_maybeData, T_index], T_maybeData),
-        T_maybeData,
+        T.fn([T_maybeDataO, T_index], T.any),
+        T_maybeDataO,
         T_index],
-       T_maybeData)
+       T.any)
 
 const T_opticOf = Category => T.lazy(T_optic => T.or(
   T.nonNegative,
   T.string,
   T.array(T_optic),
-  T.fn([T_maybeData, T_index], T_maybeData),
+  T.fn([T_maybeDataO, T_index], T_maybeDataI),
   T_opticFnOf(Category)
 ))
 
@@ -60,11 +67,11 @@ export const toFunction = T.fn([T_optic],
 // Operations on optics
 
 export const modify = T.fn([T_optic,
-                            T.fn([T_maybeData, T_index], T_maybeData),
-                            T_maybeData],
-                           T_maybeData)
-export const remove = T.fn([T_optic, T_maybeData], T_maybeData)
-export const set = T.fn([T_optic, T_maybeData, T_maybeData], T_maybeData)
+                            T.fn([T_maybeDataO, T_index], T_maybeDataI),
+                            T_maybeDataI],
+                           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)
 
 // Sequencing
 
@@ -76,11 +83,11 @@ export const compose = T.fnVar(T_optic, T_optic)
 
 // Querying
 
-export const chain = T.fn([T.fn([T_data, T_index], T_optic), T_lens], T_optic)
+export const chain = T.fn([T.fn([T_dataO, T_index], T_optic), T_lens], T_optic)
 export const choice = T.fnVar(T_lens, T_optic)
-export const choose = T.fn([T.fn([T_maybeData, T_index], T_optic)], T_optic)
+export const choose = T.fn([T.fn([T_maybeDataO, T_index], T_optic)], T_optic)
 export const optional = T_optic
-export const when = T.fn([T.fn([T_maybeData, T_index], T.any)], T_optic)
+export const when = T.fn([T.fn([T_maybeDataO, T_index], T.any)], T_optic)
 export const zero = T_optic
 
 // Recursing
@@ -93,55 +100,55 @@ export const log = T.fnVar(T.string, T_optic)
 
 // Operations on traversals
 
-export const concat = T.fn([T_monoid, T_traversal, T_maybeData], T.any)
-export const concatAs = T.fn([T.fn([T_maybeData, T_index], T.any),
+export const concat = T.fn([T_monoid, T_traversal, T_maybeDataI], T.any)
+export const concatAs = T.fn([T.fn([T_maybeDataO, T_index], T.any),
                               T_monoid,
                               T_traversal,
-                              T_maybeData],
+                              T_maybeDataI],
                              T.any)
 
 // Folds over traversals
 
-export const all = T.fn([T.fn([T_maybeData, T_index], T.any),
+export const all = T.fn([T.fn([T_maybeDataO, T_index], T.any),
                          T_traversal,
-                         T_maybeData],
+                         T_maybeDataI],
                         T.boolean)
-export const and = T.fn([T_traversal, T_maybeData], T.boolean)
+export const and = T.fn([T_traversal, T_maybeDataI], T.boolean)
 
-export const any = T.fn([T.fn([T_maybeData, T_index], T.any),
+export const any = T.fn([T.fn([T_maybeDataO, T_index], T.any),
                          T_traversal,
-                         T_maybeData],
+                         T_maybeDataI],
                         T.boolean)
 
-export const collect = T.fn([T_traversal, T_maybeData], T.array(T_data))
-export const collectAs = T.fn([T.fn([T_maybeData, T_index], T_maybeData),
+export const collect = T.fn([T_traversal, T_maybeDataI], T.array(T.def))
+export const collectAs = T.fn([T.fn([T_maybeDataO, T_index], T.any),
                                T_traversal,
-                               T_maybeData],
-                              T.array(T_data))
+                               T_maybeDataI],
+                              T.array(T.def))
 
-export const count = T.fn([T_traversal, T_maybeData], T.number)
+export const count = T.fn([T_traversal, T_maybeDataI], T.number)
 
 export const foldl =
-  T.fn([T.fn([T.any, T_maybeData, T_index], T.any),
+  T.fn([T.fn([T.any, T_maybeDataO, T_index], T.any),
         T.any,
         T_traversal,
-        T_maybeData],
+        T_maybeDataI],
        T.any)
 export const foldr = foldl
 
-export const maximum = T.fn([T_traversal, T_maybeData], T.any)
-export const minimum = T.fn([T_traversal, T_maybeData], T.any)
+export const maximum = T.fn([T_traversal, T_maybeDataI], T.any)
+export const minimum = T.fn([T_traversal, T_maybeDataI], T.any)
 
-export const or = T.fn([T_traversal, T_maybeData], T.boolean)
+export const or = T.fn([T_traversal, T_maybeDataI], T.boolean)
 
 export const product = count
 export const sum = count
 
-export const select = T.fn([T_traversal, T_maybeData], T.any)
+export const select = T.fn([T_traversal, T_maybeDataI], T.any)
 export const selectAs =
-  T.fn([T.fn([T_maybeData, T_index], T.any),
+  T.fn([T.fn([T_maybeDataO, T_index], T.any),
         T_traversal,
-        T_maybeData],
+        T_maybeDataI],
        T.any)
 
 // Creating new traversals
@@ -155,32 +162,32 @@ export const values = T_traversal
 
 // Operations on lenses
 
-export const get = T.fn([T_lens, T_maybeData], T_maybeData)
+export const get = T.fn([T_lens, T_maybeDataI], T_maybeDataO)
 
 // Creating new lenses
 
 export const lens =
-  T.fn([T.fn([T_maybeData, T_index], T_maybeData),
-        T.fn([T_maybeData, T_maybeData, T_index], T_maybeData)],
-       lens)
+  T.fn([T.fn([T_maybeDataO, T_index], T_maybeDataI),
+        T.fn([T_maybeDataO, T_maybeDataO, T_index], T_maybeDataI)],
+       T_lens)
 
 // Computing derived props
 
-export const augment = T.fn([T.props(T.fn([T.any], T_data))], T_lens)
+export const augment = T.fn([T.props(T.fn([T.any], T_maybeDataI))], T_lens)
 
 // Enforcing invariants
 
-export const defaults = T.fn([T_data], T_lens)
-export const define = T.fn([T_data], T_lens)
-export const normalize = T.fn([T.fn([T_data, T_index], T_data)], T_lens)
-export const required = T.fn([T_data], T_lens)
-export const rewrite = T.fn([T.fn([T_data, T_index], T_data)], T_lens)
+export const defaults = T.fn([T_dataI], T_lens)
+export const define = T.fn([T_dataI], T_lens)
+export const normalize = T.fn([T.fn([T_dataO, T_index], T_maybeDataI)], T_lens)
+export const required = T.fn([T_dataI], T_lens)
+export const rewrite = T.fn([T.fn([T_dataO, T_index], T_maybeDataI)], T_lens)
 
 // Lensing arrays
 
 export const append = T_lens
-export const filter = T.fn([T.fn([T_data, T_index], T.any)], T_lens)
-export const find = T.fn([T.fn([T_data, T_index], T.any)], T_lens)
+export const filter = T.fn([T.fn([T_maybeDataO, T_index], T.any)], T_lens)
+export const find = T.fn([T.fn([T_maybeDataO, T_index], T.any)], T_lens)
 export const findWith = T.fnVar(T_lens, T_lens)
 export const index = T.fn([T.nonNegative], T_lens)
 export const last = T_lens
@@ -203,16 +210,16 @@ export const orElse = T.fn([T_lens, T_lens], T_lens)
 // Transforming data
 
 export const pick = T.fn([T.props(T_lens)], T_lens)
-export const replace = T.fn([T_maybeData, T_maybeData], T_lens)
+export const replace = T.fn([T_maybeDataI, T_maybeDataI], T_lens)
 
 // Operations on isomorphisms
 
-export const getInverse = T.fn([T_isomorphism, T_maybeData], T_maybeData)
+export const getInverse = T.fn([T_isomorphism, T_maybeDataI], T_maybeDataO)
 
 // Creating new isomorphisms
 
-export const iso = T.fn([T.fn([T_maybeData], T_maybeData),
-                         T.fn([T_maybeData], T_maybeData)],
+export const iso = T.fn([T.fn([T_maybeDataO], T_maybeDataI),
+                         T.fn([T_maybeDataO], T_maybeDataI)],
                         T_isomorphism)
 
 // Isomorphisms and combinators