A simple, concise, and flexible immutable manipulation library. It's heavily inspired by lenses, but it avoids some of their restrictiveness.
NOTE: This is probably completely and horribly broken, and it's wholly untested and incredibly incomplete. Don't use this in production.
Pretty simple:
npm install --save isiahmeadows/immutable
yarn add github:isiahmeadows/immutable
Each of these are available under immutable
:
immutable/core
, re-exported directlyimmutable/async
, re-exported under the namespaceA
These are available via immutable/core
create(view): newObject
- Returnsview.create()
get(object, view): value
- Returnsview.get(object)
set(object, view, value): newObject
- Returnsview.set(object, value)
update(object, view, func): newObject
- Returnsview.update(object, func)
orview.set(object, func(view.get(object)))
has(object, view): boolean
- Returnsview.has(object)
, coerced to a booleanremove(object, view): newObject
- Returnsview.remove(object)
i(object): wrapper
- Wraps an object for sync manipulation using the above methods
Wrappers have various methods that mirror core methods, plus a couple to get the unwrapped value:
wrapper.get(view)
- Same asi(get(wrapper.object, view))
wrapper.set(view, value)
- Same asi(set(wrapper.object, view, value))
wrapper.update(view, func)
- Same asi(update(wrapper.object, view, func))
wrapper.has(view)
- Same ashas(wrapper.object, view)
wrapper.remove(view)
- Same asi(remove(wrapper.object, view))
wrapper.value
andwrapper.valueOf()
get the underlying value out of the wrapper.
There are also a couple core view factories, which exist for two reasons:
- They are very broadly useful.
- They enable certain common forms of composition.
c(...views)
- Compose multiple views into one nested viewp({...keys})
- Split and join multiple views based on object keys
Here's how each of these work:
-
c(...views)
proxy all their operations throughviews
.create
usescreate
andupdate
to create a composed view.get
recurses throughget
, returningundefined
if any returnsnull
/undefined
.set
works identically toupdate
with a thunk returning the value to set, but with preference forset
at the last step.update
recurses throughupdate
(falling back toget
+set
pairs) to eventually update the value.has
recurses throughget
for all but the last view, in which it callshas
. If anyget
returnsnull
/undefined
, it returnsfalse
and aborts recursion.remove
recurses throughupdate
for all but the last view, in which it callsremove
.- For two special cases:
c()
returnsundefined
c(view)
returnsview
directly
-
p({...keys})
split and join all their operations through the views inkeys
.create
returns an object with each key set to the result ofcreate
ing their respective views.get
returns an object where eachkey
ofkeys
is read fromobject
and run through the corresponding view'sget
.set
returns a copy ofobject
where eachkey
ofkeys
is updated viaset
.update
is not implemented, to delegate to the fallback ofget
+set
.has
callshas
on every key's view and returnstrue
if any returntrue
,false
otherwise.remove
callsremove
on each key's view.
This uses views to control how to read and write values. They take three forms:
-
A single
{create, get, set, update, has, remove}
object:v.create() -> newObject
creates a new empty instance.v.get(object) -> value
gets the underlying value of the view.v.set(object, value) -> newObject
sets the underlying value in a new clonev.update(object, func) -> newObject
is a fusedv.set(object, func(v.get(object)))
, in case this can be more efficiently optimized.v.has(object) -> boolean
tests if the view exists on an object.v.remove(object) -> newObject
removes the view from an object.- All methods are optional, and need not be own.
- Invariants:
object
could benull
/undefined
inset
andupdate
, but in nothing else.func
inv.update(object, func)
may be called any number of times - it's not restricted to just one call.- If both
v.set(object, value)
andv.update(object, func)
are present,v.set(object, value)
must have the same effect asv.update(object, () => value)
.
- As a general rule of thumb,
v.update(object, func)
should be equivalent tov.set(object, func(v.get(object)))
unless the view is a cursor over multiple items.
-
Property keys:
create
returns[]
ifview
is an integer index,{}
otherwise.get
returnsobject[view]
set
returnsObject.assign(object.slice(), {[view]: value})
ifview
is an integer index,{...object, [view]: value}
otherwise.has
returnsview in object
remove
returnso
inlet o = object.slice(); o.splice(view, 1)
ifview
is an integer index,o
inlet o = {...object}; delete o[view]
otherwise.- Note: integer indices are
value
s wheretypeof value === "number" && value % 1 === 0 && 1 / value === Infinity && value <= Number.MAX_SAFE_INTEGER
-
null
/undefined
:create
returnsundefined
get
returnsobject
.set
returnsvalue
.has
returnstrue
.remove
returnsobject
.- Note: this functions more or less as the "self" view.
If you're from a functional programming background, you're probably thinking that the view object variant looks a lot like a lens, and you would be right - it's functionally a superset (all you'd need is a {get, set}
pair to emulate one). There's three primary differences here:
get
andset
don't both need to exist - you can have just aget
or just aset
and it still work. With lenses, this is not the case.- You can manipulate nested entries as if they were individual values.
- You can manipulate not just the value, but existence itself with
has
andremove
, like with a set key. Lenses deal with exclusively values, not existence.
In general, it makes sense to implement has
if you implement remove
and get
if you implement set
or update
- it makes sense to implement the reading variant without the modifying variant, like has
without remove
, but it doesn't make sense to implement remove
without has
unless the data is logically read-only. There are exceptions, particularly when it's a truly write-only view like concat
, but these exceptions are relatively rare.
These are available via immutable/core
These exist because they cover relatively common use cases, but they are separate because they aren't critical.
Each of these return paths.
head
- Shift/unshift arrays, read first itemtail
- Push/pop arrays, read last itemitem(key)
, alias:count(key)
- Includeskey
, count occurrences ofkey
, remove all occurrences ofkey
filter(selector)
- Matchesselector
, get items matchingselector
, replace all items matchingselector
, remove all values matchingselector
reject(selector)
- Doesn't matchselector
, get items not matchingselector
, replace all items not matchingselector
, remove all values not matchingselector
each
- View all entries individually.firstItem(key)
- Includeskey
, remove first occurrence ofkey
slice(start, end)
- View a sliceconcat
- Concat arraysreverse
- View reversedsetAdd
- Set addsetKey(key)
- Set has/removemapKey(key)
- Map has/add/removeinvoke(method, ...args)
- Invoke method
Here's how each of these work:
-
head
views the first element of an array:create
returns[]
.get
returns the first item ofobject
, orundefined
if it's empty.set
prepends the value toobject
.has
returnstrue
ifobject
is non-empty,false
otherwise.remove
removes the first value fromobject
.
-
tail
views the last element of an array:create
returns[]
.get
returns the last item ofobject
, orundefined
if it's empty.set
appends the value toobject
.has
returnstrue
ifobject
is non-empty,false
otherwise.remove
removes the last value fromobject
.
-
item(key)
views a cursor on the array based on the value of an item.create
is not implemented. (This acts as a multiset-like key, not a map-like key.)get
returns the number of occurrences ofkey
inobject
.set
is not implemented. (This acts as a multiset-like key, not a map-like key.)has
returnstrue
ifobject
includeskey
,false
otherwise.remove
removes all occurrences ofkey
inobject
.
-
filter(selector)
views a cursor over multiple entries based on whether an item matches a selector function.create
returns[]
.get
returns the list of items whereselector(item, index)
returns truthy.set
replaces all items whereselector(item, index)
returns truthy withvalue
.update
replaces all items whereselector(item, index)
returns truthy withfunc(item)
.has
returnstrue
ifselector(item, index)
returns truthy for anyitem
,false
otherwise.remove
removes all items whereselector(item, index)
returns truthy.
-
reject(selector)
views a cursor over multiple entries based on whether an item doesn't match a selector function.create
returns[]
.get
returns the list of items whereselector(item, index)
returns falsy.set
replaces all items whereselector(item, index)
returns falsy withvalue
.update
replaces all items whereselector(item, index)
returns falsy withfunc(item)
.has
returnstrue
ifselector(item, index)
returns falsy for anyitem
,false
otherwise.remove
removes all items whereselector(item, index)
returns falsy.
-
each
views a cursor over multiple entries of an array.create
returns[]
.get
is not implemented. (Splitting is injective, not bijective.)set
replaces all items withvalue
.update
replaces all items withfunc(item)
.has
is not implemented.remove
is not implemented.
-
firstItem(key)
views a cursor on the array based on the value of an item.create
is not implemented.get
is not implemented. (This acts as a set-like key, not a map-like key.)set
is not implemented. (This acts as a set-like key, not a map-like key.)has
returnstrue
ifobject
includeskey
,false
otherwise.remove
removes the first occurrence ofkey
inobject
.
-
slice(start, end)
views a contiguous slice of an array.create
is not implemented. (This requires existing data before it can view it.)get
returnsarray.slice(start, end)
set
updates the array's entries in the view's index range with the value's firstend - start
entries.has
is not implemented.remove
is not implemented.
-
concat
views an array and only permits updating with concatenation.create
returns[]
.get
is not implemented. (Concatenation is injective, not bijective.)set
concatenates the list of values with the array.has
is not implemented.remove
is not implemented.
-
reverse
views the reversed representation of an array.create
returns[]
.get
returnsarray.slice().reverse()
.set
returnsvalue.slice().reverse()
.has
is not implemented.remove
is not implemented.
-
setAdd
views a set.create
returnsnew Set()
.get
is not implemented.set
clones the set via its constructor (and potentiallySymbol.species
) or usesnew Set()
if the original set isnull
/undefined
, invokesset.add(value)
, and returns the newly createdset
.has
is not implemented.remove
is not implemented.
-
setKey(key)
views a particular key of a set.create
is not implemented.get
is not implemented.set
is not implemented.has
returnsset.has(key)
.remove
clones the set via its constructor (and potentiallySymbol.species
), invokesset.delete(value)
, and returns the newly createdset
. If the original set isnull
/undefined
, it returnsundefined
-
mapKey(key)
views a particular key of a map.create
returnsnew Map()
.get
returnsmap.get(key)
.set
clones the map via its constructor (and potentiallySymbol.species
) or usesnew Map()
if the original set isnull
/undefined
, invokesset.add(key, value)
, and returns the newly createdset
.has
returnsmap.has(key)
.remove
clones the map via its constructor (and potentiallySymbol.species
), invokesmap.delete(key)
, and returns the newly createdset
. If the original map isnull
/undefined
, it returnsundefined
-
invoke(method, ...args)
views a method invocation on an object.create
is not implemented.get
returnsobject[method](...args)
, orundefined
if the method doesn't exist.set
is not implemented.has
is not implemented.remove
is not implemented.
If not listed above, view.update(object, func)
is implemented as an optimized version of view.set(object, func(view.get(object)))
or omitted if either get
or set
are not implemented.
const {p, c, i, set, update, remove, tail} = require("immutable/core")
// Some arbitrary structure
const thing = {
foo: 'bar',
fizz: 'buzz',
bish: 'bash',
utils: {
mean(...set) {
let sum = 0
for (let i = 0; i < set.length; i++) sum += set[i] / set.length
return sum
},
fibonacci(x) {
let a = 0, b = 1
for (let i = 2; i <= x; i++) {
let c = a + b
a = b
b = c
}
return b
},
},
stupidly: {
deep: {
structure: ['lol']
},
with: ['a', 'list', 'tacked', 'on'],
},
}
// A deep patch
// Change the value of `foo`
thing = set(thing, "foo", "baz")
// Delete property `bish`
thing = remove(thing, "bish")
// Memoize `fibonacci`
thing = update(thing, c("utils", "fibonacci"), fibonacci => {
const cache = Object.create(null)
return x => x in cache ? cache[x] : cache[x] = fibonacci(x)
})
// ['lol', 'roflmao'] - it's appended to the end
thing = set(thing, c("stupidly", "deep", "structure", tail), "roflmao")
// ['a', 'copy', 'tacked', 'on'] - the original array is left untouched
thing = set(thing, c("stupidly", "with", 1), "copy")
// A deep patch with chaining
thing = i(thing)
// Change the value of `foo`
.set("foo", "baz")
// Delete property `bish`
.remove("bish")
// Memoize `fibonacci`
.update(c("utils", "fibonacci"), fibonacci => {
const cache = Object.create(null)
return x => x in cache ? cache[x] : cache[x] = fibonacci(x)
})
// ['lol', 'roflmao'] - it's appended to the end
.set(c("stupidly", "deep", "structure", tail), "roflmao")
// ['a', 'copy', 'tacked', 'on'] - the original array is left untouched
.set(c("stupidly", "with", 1), "copy")
.value
// A deep patch using a split view
const fibonacci = thing.utils.fibonacci
const cache = Object.create(null)
thing = set(remove(thing, "bish"), p({
foo: undefined,
utils: "fibonacci",
stupidly: p({
deep: c("structure", tail),
with: 1,
}),
}), {
foo: "baz"
utils: x => x in cache ? cache[x] : cache[x] = fibonacci(x),
stupidly: {deep: "roflmao", with: "copy"}
})
These are available via immutable/async
These exist to help simplify backend data handling, querying, and other async operations.
A.create(view): Promise<newObject>
A.has(object, view): Promise<boolean>
A.get(object, view): Promise<value>
A.set(object, view, value): Promise<newObject>
A.update(object, view, func): Promise<newObject>
A.remove(object, view): Promise<newObject>
A.c(...paths): view
A.p(...paths): view
A.i(object): wrapper
These are equivalent to their identically-named immutable/core
counterparts, but has
/get
/set
/update
/remove
all return promises to their values, and they all await all their intermediate results, including basic has
/get
/set
/update
/remove
calls. A.i
's wrapper invokes the A.*
methods instead of the standard ones and returns promises to wrappers from the get
/set
/update
/remove
methods rather than raw wrappers itself.
These may seem simple and nothing much, but you could easily define a handler for things like MongoDB queries, remote resources, among other things, for much easier manipulation of them.