This is intended to be a minimilistic implementation of Atoms. We will define an Atom as:
A mutable reference to objects which are treated as immutable.
It may be useful to also define 3 specific types of atoms.
root atom:
The basic atom, which has no dependencies on other atoms. Always writable.
lensed atom:
An atom which views a slice of another atom. It is writable if its source is writable. Writes will propagate to the top-most writable source atom.
computed atom:
Computes from other atoms. Always readonly.
This library intends to focus on being a powerful tool, while maintaing a small footprint and staying approachable. If you would like to use streams/atoms but fear about bloat then this may be a good choice for you.
The browser build (as of 9/22/2017) clocks in at 2.5kB gzipped:
> gzip -c dist/atom.js | wc -c
2559
You can install @isthmus/atom with:
npm install --save @isthmus/atom
Then to use in your JavaScript:
import { Atom, combine, scanMerge } from '@isthmus/atom'
// and then use...
There is a browser version available under @isthmus/atom/dist/atom.js
:
<script src="https//unpkg.com/@isthmus/atom/dist/atom.js"></script>
<script>
const { Atom, scan, HALT } = isthmus.atom
// ...
</script>
Note that all functions without optional arguments are curried with crry
.
- Atom
- combine
- map
- view
- get
- set
- over
- remove
- isAtom
- modify
- scan
- merge
- scanMerge
- end
- log
- toJSON
- valueOf
- HALT
Atom(any?) -> atom
Atom
is a function which returns a root atom. It optionally can receive an initial value as its argument.
const state = Atom({ foo: { bar: [1, 2, 3] } })
atom(any?) -> atom | any
atom
s are functions which can be called with an argument to set its current value.
const atom = Atom(1) // 1
atom(7) // 7
or called with no argument to retrieve its value:
x = atom() + 1 // 8
combine(fn, sources[]) -> computed atom
combine
is a function which returns a computed atom from one or more atoms.
const n = Atom(7)
const m = Atom(2)
const sum = combine(R.add, [n, m]) // 9
m(3) // sum: 10
map(fn, source) -> computed atom
map
is an aliased version of combine for when mapping from just one atom.
const n = Atom(3)
const sqr = map(x => x * x, n) // 9
for convenience, each atom has a map
method bound to it:
const doubled = n.map(R.multiply(2)) // 6
It is also inclued as atom['fantasyland/map']
to support the fantasyland Functor spec.
view(lens, atom) --> lensed atom
view
returns a lensed atom. It can receive a string, number or array of the two which will be used as a path.
const source = Atom({ foo: [1, 2, 3] })
const lensed = view(['foo', 1], source) // 2
lensed(7) // 7; source: { foo: [1, 7, 3] }
const mapped = map(R.identity, source)
const readonly = view(['foo', 1], mapped) // 2; is readonly so readonly(7) -> Error
undefined
or null
as a lens, or in a lens path, will be treated like an identity lens. This can be useful for making separate trees that may be ended simultaneously.
const source = Atom({ foo: [1, 2, 3] })
const lensed = view('foo', source)
const proxy = view(null, source)
const proxiedLensed = view('foo', proxy)
proxy.end() // proxiedLensed is ended, lensed is still alive
for convenience, each atom has a view
method bound to it:
const atom = Atom(['a', 'b', 'c'])
const head = atom.view(0) // 'a'
get(lens, atom) --> value
get
allows you to retrieve an atom's value by supplied lens.
const source = Atom({ foo: [7, 9, 11] })
get(['foo', 2], source) // 11
for convenience, each atom has a get
method bound to it:
source.get(['foo', 1]) // 9
set(lens, value, atom) --> atom
set
allows you to change an atom's value by supplied lens. It returns the affected atom.
const source = Atom({ foo: [1, 2, 3] })
set(['foo', 1], 7, source) // { foo: [1, 7, 3] }
for convenience, each atom has a set
method bound to it:
source.set(['foo', 2], 11) // { foo: [1, 7, 11] }
over(lens, fn, atom) --> atom
over
allows you to change an atom's value by supplied lens by applying a function to it. It returns the affected atom.
const source = Atom({ foo: [1, 2, 3] })
over(['foo', 1], x => x - 3, source) // { foo: [1, -1, 3] }
for convenience, each atom has an over
method bound to it:
source.over(['foo', 2], x => x * 2) // { over: [1, -1, 6] }
remove(atom) -> atom
remove
sets an atom to undefined
, and returns the atom. If passed to a lensed atom, this means removing the property/array item from its source.
const atom = Atom({ a: 0, b: 0, c: 7 })
const b = atom.view('b')
remove(b) // undefined, atom: { a: 0, c: 7 }
for convenience, each atom has a remove
method bound to it:
const atom = Atom(['a', 'b', 'c'])
const head = atom.view(0) // 'a'
head.remove() // undefined, atom: ['b', 'c']
isAtom(any) -> boolean
isAtom
is a helper function which returns if the given argument is an atom.
const source = Atom([1, 2, 3]) // [1, 2, 3]
const computed = map(x => ({ x }), source) // { x: [1, 2, 3] }
const lensed = view(['x', 1], computed) // 2
isAtom(source) // true
isAtom(computed) // true
isAtom(lensed) // true
isAtom(123) // false
isAtom('my string') // false
isAtom({ foo: 'bar' }) // false
modify(fn, atom) -> atom
modify
accepts a function which will receive the current value of the given atom. Its returned value will be set on the atom.
It returns the atom being modified so it can be chained.
const atom = Atom(7)
modify(R.add(3), atom) // atom: 10
for convenience, each atom has a modify
method bound to it:
atom.modify(R.add(10)) // atom: 20
scan(fn, seed, atom) -> computed atom
scan
accepts an accumulator function and seed. Note that it fires immediately with any current value of its parent atom.
const nums = Atom(1)
const sum = scan((prev, next) => prev + next, 0, nums) // 1
for convenience, each atom has a scan
method bound to it:
const clicks = Atom()
const clickCount = clicks.scan(R.add(1), -1) // 0
document.addEventListener('click', clicks)
merge(atom1, atom2) -> computed atom
merge
returns a computed atom from two sources. It has the value of whichever last updated. It will initialize as the value of the second atom passed in.
const atom1 = Atom(1)
const atom2 = Atom('a')
const merged = merge(atom1, atom2) // 'a'
atom1(1) // merged: 1
scanMerge(pairs, seed)
scanMerge
accepts an array of arrays which contain an accumulator function and its atom, and a seed.
The resulting atom will update whenever one of its sources does according to the related accumulator function.
const s1 = Atom(0)
const s2 = Atom(0)
const s3 = Atom(0)
const scanmerged = scanMerge([
[R.add, s1],
[R.multiply, s2],
[R.subtract, s2]
], 0) // 0
s1(2) // scanmerged: 2
s2(3) // scanmerged: 6
s3(8) // scanmerged: -2
log(values[])
log
is a helper function intended for development use which logs values given to it. If they are an atom, it will print its current value.
const atom = Atom('bar')
log('foo', atom) // > foo bar
for convenience, each atom has a log
method bound to it:
atom.log() // > 'bar'
For serialization, each atom has a toJSON
method.
const value = Atom(123)
const serialized = JSON.stringify(value)
console.log(serialized) // > 123
(example borrowed from mithril)
For serialization, each atom has a valueOf
method.
const value = Atom(123)
console.log('test ' + value) // > "test 123"
(example borrowed from mithril)
HALT
is a string constant which has two purposes.
- To create "cold" Atoms, that is: computed atoms will not fire immediately (seeds will be used as intial values in the case of scan, scanMerge):
const atom = Atom(HALT)
const count = scan(R.add(1), 0, atom) // 0
document.addEventListener('click', atom)
// click -> 1
// click -> 2
- To be returned in a computed atoms compute function in order to prevent children from updating.
const a = Atom(1)
const b = map(val => val % 2 === 0 ? HALT : val)(a)
const c = Atom(2)
const d = combine(R.add, [b, c]) // 3
a(2) // b: HALT, d: 3
c(4) // d: 3
a(3) // b: 3, d: 7
Notes:
- Even one source having a
HALT
value will stop all updates for descendants. HALT
should not be set viaatom(HALT)
. This will throw an error.HALT
is not intended to be used with lensed atoms.
end(atom)
end
turns off an atom and its descendants. This will allow its data to be garbage collected.
For convenience, each atom has a bound end
method
const atom = Atom(foo)
// later
atom.end()
atom1.ap(atom2)
This is included as atom['fantasyland/ap']
to support the fantasyland Apply spec.
It is aliased as atom.ap
for convenience.
const y = Atom(7)
const u = Atom(x => x * x)
y.ap(u) // 49
Atom.of(atom2)
This is included as Atom['fantasyland/of']
to support the fantasyland Applicative spec.
It is aliased as Atom.of
for convenience.
const x = Atom.of(7) // same as Atom(7)
fantasyland is a set of specifications for algebraic structures to promote interop between libraries.
@isthmus/atom draws inspiration from mithril streams, flyd and calmm-js. It utilizes crry and @isthmus/optics as its dependencies.