Giter VIP home page Giter VIP logo

icepick's Introduction

icepick

A tiny (1kb min/gzipped), zero-dependency library for treating frozen JavaScript objects as persistent immutable collections.

Build Status via Travis CI NPM version Coverage Status

Motivation

Object.freeze() is a quick and easy way to get immutable collections in plain JavaScript. If you recursively freeze an object hierarchy, you have a nice structure you can pass around without fear of mutation. The problem is that if you want to modify properties inside this hierarchical collection, you have to return a new copy with the properties changed.

A quick and dirty way to do this is to just _.cloneDeep() or JSON.parse(JSON.stringify()) your object, set the new properties, and re-freeze, but this operation is expensive, especially if you are only changing a single property in a large structure. It also means that all the branches that did not have an update will be new objects.

Instead, what icepick does is provide functions that allow you to "modify" a frozen structure by returning a partial clone using structural sharing. Only collections in the structure that had a child change will be changed. This is very similar to how Clojure's persistent data structures work, albeit more primitive.

icepick uses structural sharing at the object or array level. Unlike Clojure, icepick does not use tries to store objects or arrays, so updates will be less efficient. This is to maintain JavaScript interoperability at all times. Also, for smaller collections, the overhead of creating and managing a trie structure is slower than simply cloning the entire collection. However, using very large collections (e.g.collections with more than 1000 elements) with icepick could lead to performance problems.

Structural sharing is useful wherever you can avoid expensive computation if you can quickly detect if the source data has changed. For example, shouldComponentUpdate in a React component. If you are using a frozen hierarchical object to build a system of React components, you can be confident that a component doesn't need to update if its current props strictly equal the nextProps.

API

  • freeze
  • thaw
  • assoc
  • set
  • dissoc
  • unset
  • assocIn
  • setIn
  • getIn
  • updateIn
  • push
  • unshift
  • pop
  • shift
  • reverse
  • sort
  • splice
  • slice
  • map
  • filter
  • assign
  • extend
  • merge
  • chain

Usage

icepick is provided as a CommonJS module with no dependencies. It is designed for use in Node, or with module loaders like Browserify or Webpack. To use as a global or with require.js, use icepick.min.js or icepick.dev.js directly in a browser.

$ npm install icepick --save
"use strict"; // so attempted modifications of frozen objects will throw errors

var icepick = require("icepick");

The API is heavily influenced from Clojure/mori. In the contexts of these docs "collection" means a plain, frozen Object or Array. Only JSON-style collections are supported. Functions, Dates, RegExps, DOM elements, and others are left as-is, and could mutate if they exist in your hierarchy.

If you set process.env.NODE_ENV to "production" in your build, using envify or its equivalent, freezing objects will be skipped. This can improve performance for your production build.

freeze(collection)

Recursively freeze a collection and all its child collections with Object.freeze(). Values that are not plain Arrays or Objects will be ignored, including objects created with custom constructors (e.g. new MyClass()). Does not allow reference cycles.

var coll = {
  a: "foo",
  b: [1, 2, 3],
  c: {
    d: "bar"
  }
};

icepick.freeze(coll);

coll.c.d = "baz"; // throws Error

var circular = {bar: {}};
circular.bar.foo = circular;

icepick.freeze(circular); // throws Error

thaw(collection)

Recursively un-freeze a collection by creating a partial clone. Object that are not frozen or that have custom prototypes are left as-is. This is useful when interfacing with other libraries.

var coll = icepick.freeze({a: "foo", b: [1, 2, 3], c: {d: "bar"}, e: new Foo() });
var thawed = icepick.thaw(coll);

assert(!Object.isFrozen(thawed));
assert(!Object.isFrozen(thawed.c));
assert(thawed.c !== coll.c);
assert(thawed.e === coll.e);

assoc(collection, key, value)

alias: set

Set a value in a collection. If value is a collection, it will be recursively frozen (if not already). In the case that the collection is an Array, the key is the array index.

var coll = {a: 1, b: 2};

var newColl = icepick.assoc(coll, "b", 3); // {a: 1, b: 3}


var arr = ["a", "b", "c"];

var newArr = icepick.assoc(arr, 2, "d"); // ["a", "b", "d"]

dissoc(collection, key)

alias: unset

The opposite of assoc. Remove the value with the key from the collection. If used on an array, it will create a sparse array.

var coll = {a: 1, b: 2, c: 3};

var newColl = icepick.dissoc(coll, "b"); // {a: 1, c: 3}

var arr = ["a", "b", "c"];

var newArr = icepick.dissoc(arr, 2); // ["a", , "c"]

dissocIn(collection, path)

alias: unsetIn

The opposite of assocIn. Remove a value inside a hierarchical collection. path is an array of keys inside the object. Returns a partial copy of the original collection.

var coll = {a: 1, b: {d: 5, e: 7}, c: 3};

var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {e: 7}}, c: 3}

var coll = {a: 1, b: {d: 5}, c: 3};

var newColl = icepick.dissocIn(coll, ["b", "d"]); // {a: 1, {b: {}}, c: 3}

var arr = ["a", "b", "c"];

var newArr = icepick.dissoc(arr, [2]); // ["a", , "c"]

assocIn(collection, path, value)

alias: setIn

Set a value inside a hierarchical collection. path is an array of keys inside the object. Returns a partial copy of the original collection. Intermediate objects will be created if they don't exist.

var coll = {
  a: "foo",
  b: [1, 2, 3],
  c: {
    d: "bar"
  }
};

var newColl = icepick.assocIn(coll, ["c", "d"], "baz");

assert(newColl.c.d === "baz");
assert(newColl.b === coll.b);

var coll = {};
var newColl = icepick.assocIn(coll, ["a", "b", "c"], 1);
assert(newColl.a.b.c === 1);

getIn(collection, path)

Get a value inside a hierarchical collection using a path of keys. Returns undefined if the value does not exist. A convenience method -- in most cases plain JS syntax will be simpler.

var coll = icepick.freeze([
  {a: 1},
  {b: 2}
]);

var result = icepick.getIn(coll, [1, "b"]); // 2

updateIn(collection, path, callback)

Update a value inside a hierarchical collection. The path is the same as in assocIn. The previous value will be passed to the callback function, and callback should return the new value. If the value does not exist, undefined will be passed. If not all of the intermediate collections exist, an error will be thrown.

var coll = icepick.freeze([
  {a: 1},
  {b: 2}
]);

var newColl = icepick.updateIn(coll, [1, "b"], function (val) {
  return val * 2;
}); // [ {a: 1}, {b: 4} ]

assign(coll1, coll2, ...)

alias: extend

Similar to Object.assign, this function shallowly merges several objects together. Properties of the objects that are Objects or Arrays are deeply frozen.

var obj1 = {a: 1, b: 2, c: 3};
var obj2 = {c: 4, d: 5};

var result = icepick.assign(obj1, obj2); // {a: 1, b: 2, c: 4, d: 5}
assert(obj1 !== result); // true

merge(target, source, [associator])

Deeply merge a source object into target, similar to Lodash.merge. Child collections that are both frozen and reference equal will be assumed to be deeply equal. Arrays from the source object will completely replace those in the target object if the two differ. If nothing changed, the original reference will not change. Returns a frozen object, and works with both unfrozen and frozen objects.

var defaults = {a: 1, c: {d: 1, e: [1, 2, 3], f: {g: 1}}};
var obj = {c: {d: 2, e: [2], f: null}};

var result1 = icepick.merge(defaults, obj); // {a: 1, c: {d: 2, e: [2]}, f: null}

var obj2 = {c: {d: 2}};
var result2 = icepick.merge(result1, obj2);

assert(result1 === result2); // true

An optional resolver function can be given as the third argument to change the way values are merged. For example, if you'd prefer that Array values from source be concatenated to target (instead of the source Array just replacing the target Array):

var o1 = icepick.freeze({a: 1, b: {c: [1, 1]}, d: 1});
var o2 = icepick.freeze({a: 2, b: {c: [2]}});

function resolver(targetVal, sourceVal, key) {
  if (Array.isArray(targetVal) && sourceVal) {
    return targetVal.concat(sourceVal);
  } else {
    return sourceVal;
  }
}

var result3 = icepick.merge(o1, o2, resolver);
assert(result === {a: 2, b: {c: [1, 1, 2]}, d: 1});

The resolver function receives three arguments: the value from the target object, the value from the source object, and the key of the value being merged.

Array.prototype methods

  • push
  • pop
  • shift
  • unshift
  • reverse
  • sort
  • splice

Each of these mutative Array prototype methods have been converted:

var a = [1];
a = icepick.push(a, 2); // [1, 2];
a = icepick.unshift(a, 0); // [0, 1, 2];
a = icepick.pop(a); // [0, 1];
a = icepick.shift(a); // [1];
  • slice(arr, start, [end])

slice is also provided as a convenience, even though it does not mutate the original array. It freezes its result, however.

  • map(fn, array)
  • filter(fn, array)

These non-mutative functions that return new arrays are also wrapped for convenience. Their results are frozen. Note that the mapping or filtering function is passed first, for easier partial application.

icepick.map(function (v) {return v * 2}, [1, 2, 3]); // [2, 4, 6]

var removeEvens = _.partial(icepick.filter, function (v) { return v % 2; });

removeEvens([1, 2, 3]); // [1, 3]

Array methods like find or indexOf are not added to icepick, because you can just use them directly on the array:

var arr = icepick.freeze([{a: 1}, {b: 2}]);

arr.find(function (item) { return item.b != null; }); // {b: 2}

chain(coll)

Wrap a collection in a wrapper that allows calling icepick function as chainable methods, similar to lodash.chain. This is convenient when you need to perform multiple operations on a collection at one time. The result of calling each method is passed to the next method in the chain as the first argument. To retrieve the result, call wrapped.value(). Unlike lodash.chain, you must always call .value() to get the result, the methods are not lazily evaluated, and intermediate collections are always created (but this may change in the future).

var o = {
  a: [1, 2, 3],
  b: {c: 1},
  d: 4
};

var result = icepick.chain(o)
  .assocIn(["a", 2], 4)
  .merge({b: {c: 2, c2: 3}})
  .assoc("e", 2)
  .dissoc("d")
  .value();

expect(result).to.eql({
  a: [1, 2, 4],
  b: {c: 2, c2: 3},
  e: 2
});

The wrapper also contains an additional thru method for performing arbitrary updates on the current wrapped value.

var result = icepick.chain([1, 2])
  .push(3)
  .thru(function (val) {
    return [0].concat(val)
  })
  .value(); // [0, 1, 2, 3]

FAQ

Why not just use Immutable.js or mori?

Those two libraries introduce their own types. If you need to share a frozen data structure with other libraries or other 3rd-party code, you force those downstream from you to use Immutable.js or mori (and in the case of mori, the exact version you use). Also, since you can build your data structures using plain JS, creating the initial representation is faster. The overhead ofObject.freeze() is negligible.

How does this differ from React.addons.update or seamless-immutable.

All three of these libraries are very similar in their goals -- provide incremental updates of plain JS objects. They mainly differ in their APIs.

React.addons.update provides a single function to which you pass an object of commands. While this can be convenient to do many updates in a single batch, the syntax of the command object is very cumbersome, especially when dealing with computed property names. It also does not freeze the objects it operates on, leaving them open to modifications elsewhere in your code.

seamless-immutable is the most similar to icepick. Its main difference is that it adds methods to the prototypes of objects, and overrides array built-ins like map and filter to return frozen objects. It also adds a couple utility functions, like asMutable and merge. icepick does not modify the methods or properties of collections in order to function, it merely provides a set of functions to operate on them, similar to Lodash, Underscore, or Ramda. This means that when passing frozen objects to third-party libraries, they will be able to map over them and obtain mutable arrays. seamless-immutable handles Dates, which icepick leaves as-is currently (as well as any other objects with custom constructors). icepick will detect circular references within an object and throw an Error, seamless-immutable will run into infinite recursion in such a case.

Isn't this horribly slow?

It is faster than deeply cloning an object. Since it does not touch portions of a data structure that did not change, it can help you optimize expensive calculations elsewhere (such as rendering a component in the DOM). It is also faster than mori1.

Here are some performance profiles of various immutable libraries, icepick is faster than most, except for writes to collections with more than 100 elements.

Won't this leak memory?

Garbage collection in modern JS engines can clean up the intermediate Objects and Arrays that are no longer needed. I need to profile memory across a wider range of browsers, but V8 can definitely handle it. Working with a collection that is about 200kb as JSON, the GC phase is only 8ms after a few hundred updates. Memory usage does fluctuate a few MBs though, but it always resets to the baseline.

License

MIT

icepick's People

Contributors

aearly avatar danny-andrews avatar dependabot[bot] avatar dmnd avatar stefanfisk avatar tlrobinson avatar tonovotny avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

icepick's Issues

Version of Icepick that does not freeze

I use Icepick on React/Redux projects for two main reasons:

  1. deep freezing makes sure that I don't introduce hard-to-track-down bugs in which state is accidentally mutated.
  2. use of the various helper functions that efficiently "update" state using structural sharing.

Typically, I deep freeze every slice in my Redux store. However, for some data-heavy apps, there's a performance hit for doing so. What would you think of a variant of Icepick (that could be used in production) that provide the same helper functions but skips the freezing?

Order changed by updateIn

Consider the following snippet, run against Icepick 2.4.0:

var icepick = require("icepick")

const BEFORE = {
  first: {
    name: "First",
    items: [{key: "first-a", value: 12}]
  },
  second: {
    name: "Second",
    items: [{key: "second-a", value: 13}]
  },
  third: {
    name: "Third",
    items: [{key: "third-a", value: 15}]
  }
};

console.log(BEFORE);

const AFTER = icepick.updateIn(BEFORE, ["second", "items"], items => [
  ...items,
  {
    key: "second-x",
    value: 42
  },
]);

console.log(AFTER);

In this case, the output is:

Object
first: Object {name: "First", items: [Object {key: "first-a", value: 12}]}
second: Object {name: "Second", items: [Object {key: "second-a", value: 13}]}
third: Object {name: "Third", items: [Object {key: "third-a", value: 15}]}

Object
third: Object {name: "Third", items: [Object {key: "third-a", value: 15}]}
second: Object {items: [Object {key: "second-a", value: 13}, Object {key: "second-x", value: 42}], name: "Second"}
first: Object {name: "First", items: [Object {key: "first-a", value: 12}]}

Notice that the order of keys has been reversed by this operation (where an item was added to the items collection of the 2nd value in the map). I realize that maps are inherently unordered, but it might still be useful if this operation didn't change the order.

mergeIn

Proposed as a shortcut, implemented as follows:

import { updateIn, merge } from 'icepick'

const mergeIn = (dest, path, source, associator) => 
  updateIn(dest, path, v => merge(v, source, associator))

Would be willing to implement. I'm not even sure if it's helpful, given how easy (and explicit) it is to construct on the fly. Just a thought.

Please add a LICENSE file

Could you please add a LICENSE file with the full text of the MIT license and the copyright holder's name? It is needed to pass a legal review. Simply specifying that the license is "MIT" is not sufficient because it's not legally definable what EXACTLY does "MIT" mean.

Thanks a lot!

Main entrypoint not compatible with ES5

I know it was done on purpose, but having the "main" entrypoint in package.json pointing to ES6 module is unfortunate. It can't be easily integrated into project that targets older browsers. Babel does not process node_modules by default and having exception for just one library pollutes the build script.

Proposal: Keep main entrypoint ES5, use "module" or "jsnext:main" for ES6

package.json

...
  "main": "icepick.js", // this is ES5 compatible module
  "module": "icepick.mjs" // this is ES6 module
...

Generic "in" operator

I find assocIn etc to be useful, and sometimes wish similar functions existed for the other functions. Here's a prototype of an at (because in is a reserved word) function and chain method that let you apply any operator to a path:

https://runkit.com/tlrobinson/5dc341aa7f9859001a1b54f4

e.x.

icepick.at(object, ["foo", "bar"]).push(4)

icepick.chain(object)
  .at(["foo", "bar"]).push(4)
  .at(["new", "object"]).assoc(5)
  .at(["new", "array"]).push(6)
  .value()

Deleting keys

Would it be possible/sensible to add the ability to delete keys?

Is it possible to get mutable data out of icepick?

There seem to be 2 camp on immutable object:

  • assume you want the native array/object method instead of wrapping them as immutable api (for better compatibility with other api that assume mutable array/object)
  • assume you want to use immutable object all the way, if you happen to need mutable array/object, there are ways to convert immutable object back to mutable.

I believe icepick is in the first camp (as opposed to immutable.js or seamless-immutable, which are likely in the 2nd camp).

Am I right in thinking icepick assume developers will pass its instance directly? Because I see no method to extract mutable data out of it.

Using icepick.js in browser directly?

hi, I'm from cdnjs. Because your lib is very popular, we want to host it on https://cdnjs.com, can I add icepick.js to cdnjs directly for browser user? or if it doesn't work, can you also provide browserified icepick.js in every git tag(released versions) so that we can use git auto-update to add your lib to cdnjs automatically.
thank you very much!

cdnjs/cdnjs#7680

Doc on github readme wrong lol

Obvious mistakes:
//In getIn section
var coll = i.freeze([
{a: 1},
{b: 2}
]);

var result = i.getIn(coll, [1, "b"]); // 2

//In merge section
var defaults = {a: 1, c: {d: 1, e: [1, 2, 3], f: {g: 1}};
var obj = {c: {d: 2, e: [2], f: null};

var result1 = i.merge(defaults, obj); // {a: 1, c: {d: 2, e: [2]}, f: null}

var obj2 = {c: {d: 2}};
var result2 = i.merge(result1, obj2);

assert(result1 === result2); // true

Add `chain` to enable chaining calls

Hi Alexander, thank a lot for icepick!

As a great fan of functional programming, I am used to chaining functions like here:

var _ = require("lodash");
// Transform [{name: Kirk, address: {country: Norway, city: Oslo}},...] to [Oslo,..]:
_.chain(crewMembers).map("address").filter({country: "Norway"}).map("city").value()

With icepick this quickly becomes difficult to read (as in old good Lisp) since I have to read it inside-out:

i.map(
  i.filter(
    i.map(crewMembers, myMakeGetter("address"))
  myMakePredicate({country: "Norway"}))
myMakeGetter("city")

It would be great if Icepick supported chaining these operations for example in the same way that lodash does witch chain ... value().
Thank you!

A typo in README

There is a following typo in REAMDE.md:

icepick does not modify the the methods or properties of collections in order to function

Uglify fails on 2.0 with RCA

See https://github.com/Psykar/test-icepick-uglify

RCA transpiles to ES5 before it attempts to uglify, but seems it's falling over on some ES6 syntax. Installing icepick@<2 does work, and I don't have the time to debug this further right now unfortunately, but the repro is pretty simple.

  • create-react-app test-icepick-uglify
  • cd test-icepick-uglify
  • yarn add icepick
  • Edit src/App.js to add an icepick import
  • yarn build
yarn build v0.24.6
$ react-scripts build 
Creating an optimized production build...
Failed to compile.

static/js/main.fd1459b7.js from UglifyJs
Unexpected token: operator (>) [./~/icepick/icepick.js:16,0][static/js/main.fd1459b7.js:11521,25]

error Command failed with exit code 1.

Alternatively, clone https://github.com/Psykar/test-icepick-uglify then yarn build and you'll also see it :)

merge doesn't work with objects that don't have a prototype.

The package query-string is generating objects that have no prototype by using Object.create(null).

I want to deep merge with those objects, but icepick is not thinking they are objects because the isObject test check for prototype.

import {merge} from 'icepick';

// build object like query-string does
let query = Object.create(null);
query['q'] = 'search-term';
const route = {url: './', query: query};

// prepare action for history
const nextPage = merge(route, {query: {page: 2}});

// what I expect to be the result
assert.same(nextPage, {url: './', query: {q: 'search-term', page: 2}});

Documentation unclear about whether icepick adds methods to `Object.prototype`

In the docs, it says seamless-immutable is the most similar to icepick. Its main difference is that it adds more methods to the prototypes of objects, but I was under the impression that icepick didn't add any methods to the prototypes of objects. If that's the case, we should delete the 'more' from above to make that clear.

Question: How does this compare to seamless-immutable?

Hello,

I wonder whether I should pick seamless-immutable or icepick. They both seem to be doing the same thing, providing utilities for "modifying" deeply frozen data structures. I can see some differences but know too little to be able to really compare them and decide. Could you be so kind and describe the advantages of seamless over icepick and vice versa? Thanks a lot!

PS: I'll ask at the other project as well

Is it by design array method like I.push(arr, data) doesn't freeze data

I discover this when trying to modify an object with structure like { a: [ {b:1}, {b:2} ] }.

The best way I can think of to push {b:3} onto a array is to:

var arr = I.push(store.a, {b:3});
I.assoc(store, 'a', arr);

Then I realize {b:3} isn't freezed in the process.

Am I doing this right? Is it by design?

(not that it's a problem, I can certainly I.freeze the data before I.push).

icepick/fp interface

would be neat to have a lodash/fp interface in icepick/fp.

with curried: fn(a, b, c) => fn(b, c, a); versions of icepick functions.

Missing dissocIn.

Just as dissoc is the pair to assoc but removes a key, dissocIn should be the pair to assocIn and remove the key at the end of a path (passed as an array).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.