https://github.com/calmm-js/kefir.atom
Composable and decomposable reactive state with lenses and Kefir
https://github.com/calmm-js/kefir.atom
compose decompose lenses model observable reactive state transaction
Last synced: 5 months ago
JSON representation
Composable and decomposable reactive state with lenses and Kefir
- Host: GitHub
- URL: https://github.com/calmm-js/kefir.atom
- Owner: calmm-js
- License: mit
- Created: 2016-02-14T09:30:01.000Z (over 9 years ago)
- Default Branch: master
- Last Pushed: 2019-02-10T13:16:04.000Z (over 6 years ago)
- Last Synced: 2025-04-11T17:49:50.810Z (6 months ago)
- Topics: compose, decompose, lenses, model, observable, reactive, state, transaction
- Language: JavaScript
- Homepage:
- Size: 348 KB
- Stars: 52
- Watchers: 6
- Forks: 5
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.md
Awesome Lists containing this project
README
[ [≡](#contents) | [Motivation](#motivation) | [Tutorial](#tutorial) |
[Reference](#reference) | [About](#about) ]This library provides a family of [concepts](#concepts) and tools for managing
[state]()
with [lenses](https://github.com/calmm-js/partial.lenses) and
[Kefir](http://rpominov.github.io/kefir/).[](http://badge.fury.io/js/kefir.atom)
[](https://badge.fury.io/bo/kefir.atom)
[](https://travis-ci.org/calmm-js/kefir.atom)
[](https://codecov.io/github/calmm-js/kefir.atom?branch=master)
[](https://david-dm.org/calmm-js/kefir.atom)
[](https://david-dm.org/calmm-js/kefir.atom?type=dev)## Contents
- [Motivation](#motivation)
- [Tutorial](#tutorial)
- [Counters are not toys!](#counters-are-not-toys)
- [Component, remove thyself!](#component-remove-thyself)
- [Pure and stateless](#pure-and-stateless)
- [Lists are simple data structures](#lists-are-simple-data-structures)
- [Items in a cart](#items-in-a-cart)
- [Items to put into the cart](#items-to-put-into-the-cart)
- [Putting it all together](#putting-it-all-together)
- [Summary](#summary)
- [Reference](#reference)
- [`Atom(value)`](#Atom 'Atom: a -> Atom a')
- [`Atom()`](#Atom-empty 'Atom: () -> Atom a')
- [`atom.get()`](#get 'AbstractMutable a :: get: () -> a')
- [`atom.modify(currentValue => newValue)`](#modify 'AbstractMutable a :: modify: (a -> a) -> ()')
- [`atom.set(value)`](#set 'AbstractMutable a :: set: a -> ()')
- [`atom.remove()`](#remove 'AbstractMutable a :: remove: () -> ()')
- [`atom.view(lens)`](#view 'AbstractMutable a :: view: PLens a b -> LensedAtom b')
- [`holding(() => ...)`](#holding 'holding: (() -> a) -> a')
- [Concepts](#concepts)
- [`AbstractMutable a :> Property a`](#class-AbstractMutable)
- [`Atom a :> AbstractMutable a`](#class-Atom)
- [`LensedAtom a :> AbstractMutable a`](#class-LensedAtom)
- [`Molecule a :> AbstractMutable (a where AbstractMutable x := x)`](#class-Molecule)
- [About](#about)
- [Implementation trade-offs](#implementation-trade-offs)
- [Related work](#related-work)## Motivation
Use of state and mutation is often considered error-prone and rightly so.
Stateful concepts are _inherently difficult_, because, unlike stateless
concepts, they include the concept of time: _state changes over time_. When
state changes, computations based on state, including copies of state, may
become invalid or inconsistent.Using this library:
- You can **_store_** state in _first-class_ objects called
[Atom](#class-Atom)s.- _This means that program components can **declare** the state they are
interested in **as parameters** and share state by passing references to
state as arguments **without copying** state._- You can declare **_decomposed_** _first-class_ [view](#view)s of state using
[lenses](https://github.com/calmm-js/partial.lenses) and **_composed_**
_first-class_ views of state as [Molecule](#class-Molecule)s.- _This means that program components can **declare precisely** the state they
are interested in as parameters **independently of the storage** of state._- You get **_consistent_** read-write access to state using [get](#get) and
[modify](#modify) operations _at any point and through all views_.- _This means that by using views, both decomposed and composed, of state you
can **avoid** copying state and the **inconsistency problems** associated
with such copying._- You can declare arbitrary **_dependent computations_** using observable
combinators from [Kefir](http://rpominov.github.io/kefir/) as
[AbstractMutable](#class-AbstractMutable)s are also
[Kefir](http://rpominov.github.io/kefir/)
[properties](http://rpominov.github.io/kefir/#about-observables).- _This means that you can **declare computations** dependent **upon state**
**independently of time** as such computation are kept consistent as state
changes over time._- You can mutate state through multiple views and multiple atomic
[modify](#modify) operations in a **_transactional_** manner by
[holding](#holding) event propagation from state changes.- _This means that you can **avoid** some
[glitches and unnecessary computations](https://en.wikipedia.org/wiki/Reactive_programming#Glitches)
of **intermediate states**._- You can **_avoid unnecessary recomputations_**, because program components can
declare precisely the state they are interested in and views of state only
propagate actual changes of state.
- _This means that **algorithmic efficiency is a feature** of this library
rather than an afterthought requiring further innovation._The rest of this README contains a [tutorial](#tutorial) to managing state using
atoms and provides a [reference](#reference) manual for this library.## Tutorial
Let's write the very beginnings of a Shopping Cart UI using atoms with the
[`karet`](https://github.com/calmm-js/karet) and via the
[`karet.util`](https://github.com/calmm-js/karet.util) libraries.Karet is simple library that allows one to embed Kefir observables into
[React](https://facebook.github.io/react/) VDOM. If this tutorial advances at a
too fast a pace, then you might want to read a longer
[introduction](https://github.com/calmm-js/documentation/blob/master/introduction-to-calmm.md)
to the approach.This example is actually a stripped down version of the Karet Shopping Cart
example that you can see live [here](https://codesandbox.io/s/8y0lwxrow0).### Counters are not toys!
So, how does one create a Shopping Cart UI?
Well, _of course_, the first thing is to write the classic counter component:
```jsx
const Counter = ({count}) => (
-
{count}
+
)
```The `Counter` component displays a `count`, which is supposed to refer to state
that contains an integer, and buttons labeled `-` and `+` that decrement and
increment the `count` using [`modify`](#modify).As you probably know, a counter component such as the above is a typical first
example that the documentation of any respectable front-end framework will give
you. Until now you may have _mistakenly_ thought that those are just toys.### Component, remove thyself!
The next thing is to write a component that can remove itself:
```jsx
const Remove = ({removable}) => (
x
)
```The `Remove` component gives you a button labeled `x` that calls
[`remove`](#remove) on the `removable` state given to it.#### Pure and stateless
At this point it might be good idea to point out that both the previous
`Counter` component and the above `Remove` component are
[referentially transparent](https://en.wikipedia.org/wiki/Referential_transparency)
aka pure functions. Furthermore, instances of `Counter` and `Remove` are
stateless. This actually applies to all components in this tutorial and most
components in real-world Calmm applications can also be pure functions whose
instantiations are stateless. First-class, decomposable, and observable state
makes it easy to store state outside of components and make the components
themselves pure and stateless.### Lists are simple data structures
Then we write a higher-order component that can display a list of items:
```jsx
const Items = ({items, Item}) => (
{U.mapElemsWithIds(
'id',
(item, key) => (
),
items
)}
)
```The `Items` component is given state named `items` that is supposed to refer to
an array of objects. From that array it then produces an unordered list of
`Item` components, passing them an `item` that corresponds to an element of the
`items` state array.### Items in a cart
We haven't actually written anything shopping cart specific yet. Let's change
that by writing a component for cart items:```jsx
const count = [L.removable('count'), 'count', L.defaults(0)]const CartItem = ({item}) => (
{U.view('name', item)}
)
```The `CartItem` component is designed to work as `Item` for the previous `Items`
component. It is a simple component that is given state named `item` that is
supposed to refer to an object containing `name` and `count` fields. `CartItem`
uses the previously defined `Remove` and `Counter` components. The `Remove`
component is simply passed the `item` as the `removable`. The `Counter`
component is given a [lensed](https://github.com/calmm-js/partial.lenses)
[view](#view) of the `count`. The `count` lens makes it so that when the `count`
property reaches `0` the whole item is removed.**_This is important:_** By using a simple lens as an adapter, we could
[plug](https://github.com/calmm-js/documentation/blob/master/introduction-to-calmm.md#imagine)
the previously defined `Counter` component into the shopping cart state.If this is the first time you encounter
[partial lenses](https://github.com/calmm-js/partial.lenses), then the
definition of `count` may be difficult to understand, but it is not very complex
at all. It works like this. It looks at the incoming object and grabs all the
properties as `props`. It then uses those to return a lens that, when written
through, will replace an object of the form `{...props, count: 0}` with
`undefined`. This way, when the `count` reaches `0`, the whole item gets
removed. After working with partial lenses for some time you will be able to
write far more interesting lenses.### Items to put into the cart
We are nearly done! We just need one more component for products:
```jsx
const count = item => [
L.find(R.whereEq({id: L.get('id', item)})),
L.defaults(item),
'count',
L.defaults(0),
L.normalize(R.max(0))
]const ProductItem = cart => ({item}) => (
{U.view('name', item)}
)
```The `ProductItem` component is also designed to work as an `Item` for the
previous `Items` component. Note that `ProductItem` actually takes two curried
arguments. The first argument `cart` is supposed to refer to cart state.
`ProductItem` also reuses the `Counter` component. This time we give it another
non-trivial lens. The `count` lens is a parameterized lens that is given an
`item` to put into the `cart`.### Putting it all together
We now have all the components to put together our shopping cart application.
Here is a list of some Finnish delicacies:```jsx
const productsData = [
{id: 1, name: 'Sinertävä lenkki 500g'},
{id: 2, name: 'Maksainen loota 400g'},
{id: 3, name: 'Maidon tapainen 0.9l'},
{id: 4, name: 'Festi moka kaffe 500g'},
{id: 5, name: 'Niin hyvää ettei 55g'},
{id: 6, name: 'Suklaa Nipponi 37g'}
]
```And, finally, here is our `Shop`:
```jsx
const Shop = ({cart, products}) => (
Products
Shopping Cart
)
```The `Shop` above uses the higher-order `Items` component twice with different
`Item` components and different lists of `items`.### Summary
For the purposes of this example we are done. Here is a summary:
- We wrote several components such as `Counter`, `Remove` and `Items` that are
not specific to the application in any way.- Each component is just one referentially transparent function that takes
(possibly reactive variables as) parameters and returns VDOM.- We composed components together as VDOM expressions.
- We used `Counter` and `Items` twice in different contexts.
- When using `Counter` we used lenses to decompose application specific state to
match the interface of the component.## Reference
Typically one only uses the default export
```js
import Atom from 'kefir.atom'
```of this library. It provides a convenience function that constructs a `new`
instance of the [`Atom`](#class-Atom) class.### [≡](#contents) [`Atom(value)`](#Atom 'Atom: a -> Atom a')
Creates a new atom with the given initial value. For example:
```js
const notEmpty = Atom('initial')
notEmpty.get()
// 'initial'
notEmpty.log()
// [property] initial
```### [≡](#contents) [`Atom()`](#Atom-empty 'Atom: () -> Atom a')
Creates a new atom without an initial value. For example:
```js
const empty = Atom()
empty.get()
// undefined
empty.log()
empty.set('first')
// [property] first
```### [≡](#contents) [`atom.get()`](#get 'AbstractMutable a :: get: () -> a')
Synchronously computes the current value of the atom. For example:
```js
const root = Atom({x: 1})
const x = root.view('x')
x.get()
// 1
```Use of `get` is discouraged: prefer to depend on an atom as you would with
ordinary Kefir properties.When `get` is called on an [`AbstractMutable`](#class-AbstractMutable) that has
a root [`Atom`](#class-Atom) that does not have a value, `get` returns the
values of those [`Atom`](#class-Atom)s as `undefined`. For example:```js
const empty = Atom()
const notEmpty = Atom('initial')
const both = new Molecule({empty, notEmpty})
both.get()
// { empty: undefined, notEmpty: 'initial' }
```### [≡](#contents) [`atom.modify(currentValue => newValue)`](#modify 'AbstractMutable a :: modify: (a -> a) -> ()')
Conceptually applies the given function to the current value of the atom and
replaces the value of the atom with the new value returned by the function. For
example:```js
const root = Atom({x: 1})
root.modify(({x}) => ({x: x - 1}))
root.get()
// { x: 0 }
```This is what happens with the basic [`Atom`](#class-Atom) implementation. What
actually happens is decided by the implementation of
[`AbstractMutable`](#class-AbstractMutable) whose `modify` method is ultimately
called. For example, the `modify` operation of [`LensedAtom`](#class-LensedAtom)
combines the function with its lens and uses the resulting function to `modify`
its source. From the point of view of the caller the end result is the same as
with an [`Atom`](#class-Atom). For example:```js
const root = Atom({x: 1})
const x = root.view('x')
x.modify(x => x - 1)
x.get()
// 0
root.get()
// { x: 0 }
```### [≡](#contents) [`atom.set(value)`](#set 'AbstractMutable a :: set: a -> ()')
`atom.set(value)` is equivalent to [`atom.modify(() => value)`](#modify) and is
provided for convenience.### [≡](#contents) [`atom.remove()`](#remove 'AbstractMutable a :: remove: () -> ()')
`atom.remove()` is equivalent to [`atom.set()`](#set), which is also equivalent
to [`atom.set(undefined)`](#set), and is provided for convenience. For example:```js
const items = Atom(['To be', 'Not to be'])
const second = items.view(1)
second.get()
// 'Not to be'
second.remove()
second.get()
// undefined
items.get()
// [ 'To be' ]
```Calling `remove` on a plain [`Atom`](#class-Atom) doesn't usually make sense,
but `remove` can be useful with [`LensedAtom`](#class-LensedAtom)s, where the
"removal" will then follow from the semantics of
[remove](https://github.com/calmm-js/partial.lenses#L-remove) on partial lenses.### [≡](#contents) [`atom.view(lens)`](#view 'AbstractMutable a :: view: PLens a b -> LensedAtom b')
Creates a new [`LensedAtom`](#class-LensedAtom) that provides a read-write view
with the lens from the original atom. Modifications to the lensed atom are
reflected in the original atom and vice verse. For example:```js
const root = Atom({x: 1})
const x = root.view('x')
x.set(2)
root.get()
// { x: 2 }
root.set({x: 3})
x.get()
// 3
```One of the key ideas that makes lensed atoms work is the compositionality of
partial lenses. See the equations here:
[`L.compose`](https://github.com/calmm-js/partial.lenses#L-compose). Those
equations make it possible not just to create lenses via composition (left hand
sides of equations), but also to create paths of lensed atoms (right hand sides
of equations). More concretely, both the `c` in```js
const b = a.view(a_to_b_PLens)
const c = b.view(b_to_c_PLens)
```and in
```js
const c = a.view([a_to_b_PLens, b_to_c_PLens])
```can be considered equivalent thanks to the compositionality equations of lenses.
Note that, for most intents and purposes, `view` is a referentially transparent
function: it does not create _new_ mutable state—it merely creates a
reference to existing mutable state.### [≡](#contents) [`holding(() => ...)`](#holding 'holding: (() -> a) -> a')
There is also a named import `holding`
```js
import {holding} from 'kefir.atom'
```which is function that is given a thunk to call while holding the propagation of
events from changes to atoms. The thunk can [`get`](#get), [`set`](#set),
[`remove`](#remove) and [`modify`](#modify) any number of atoms. After the thunk
returns, persisting changes to atoms are propagated. For example:```js
const xy = Atom({x: 1, y: 2})
const x = xy.view('x')
const y = xy.view('y')
x.log('x')
// x 1
y.log('y')
// y 2
holding(() => {
xy.set({x: 2, y: 1})
x.set(x.get() - 1)
})
// y 1
```### Concepts
The above diagram illustrates the subtype relationships between the basic
concepts- **Observable**,
- **Stream**, and
- **Property**of [Kefir](http://rpominov.github.io/kefir/#about-observables) and the concepts
added by this library- **[AbstractMutable](#class-AbstractMutable)**,
- **[Atom](#class-Atom)**,
- **[LensedAtom](#class-LensedAtom)**, and
- **[Molecule](#class-Molecule)**.The classes [`AbstractMutable`](#class-AbstractMutable), [`Atom`](#class-Atom),
[`LensedAtom`](#class-LensedAtom) and [`Molecule`](#class-Molecule) are provided
as named exports:```js
import {AbstractMutable, Atom, LensedAtom, Molecule} from 'kefir.atom'
```Note that the [default export](#Atom) is not the same as the named export
[`Atom`](#class-Atom).There are use cases where you would want to create new subtypes of
[`AbstractMutable`](#class-AbstractMutable), but it seems unlikely that you
should inherit from the other classes.#### [≡](#contents) [`AbstractMutable a :> Property a`](#class-AbstractMutable)
`AbstractMutable` is the abstract base class or interface against which most
code using atoms is actually written. An `AbstractMutable` is a Kefir
[property](http://rpominov.github.io/kefir/#about-observables) that also
provides for ability to request to [`modify`](#modify) the value of the
property. `AbstractMutable`s implicitly skip duplicates using Ramda's
[`identical`](http://ramdajs.com/0.21.0/docs/#identical) function.Note that we often abuse terminology and speak of [`Atom`](#class-Atom)s when we
should speak of `AbstractMutable`s, because [`Atom`](#class-Atom) is easier to
pronounce and is more concrete.#### [≡](#contents) [`Atom a :> AbstractMutable a`](#class-Atom)
An `Atom` is a simple implementation of an
[`AbstractMutable`](#class-AbstractMutable) that actually stores state. One can
create an `Atom` directly by explicitly giving an initial value or one can
create an `Atom` without an initial value.The _value_ stored by an `Atom` _must_ be treated as an _immutable_ object.
Instead of mutating the value stored by an `Atom`, one mutates the `Atom` by
calling [`modify`](#modify), which makes the `Atom` to refer to the new value.Note that `Atom` is not the only possible root implementation of
[`AbstractMutable`](#class-AbstractMutable). For example, it would be possible
to implement an [`AbstractMutable`](#class-AbstractMutable) whose state is
actually stored in an external database that can be observed and mutated by
multiple clients.#### [≡](#contents) [`LensedAtom a :> AbstractMutable a`](#class-LensedAtom)
A `LensedAtom` is an implementation of an
[`AbstractMutable`](#class-AbstractMutable) that doesn't actually store state,
but instead refers to a part, specified using a
[lens](https://github.com/calmm-js/partial.lenses/), of another
[`AbstractMutable`](#class-AbstractMutable). One creates `LensedAtom`s by
calling the [`view`](#view) method of an
[`AbstractMutable`](#class-AbstractMutable).#### [≡](#contents) [`Molecule a :> AbstractMutable (a where AbstractMutable x := x)`](#class-Molecule)
A `Molecule` is a special _partial_ implementation of an
[`AbstractMutable`](#class-AbstractMutable) that is constructed from a template
of abstract mutables:```js
const xyA = Atom({x: 1, y: 2})
const xL = xyA.view('x')
const yL = xyA.view('y')
const xyM = new Molecule({x: xL, y: yL})
```When read, either as a property or via [`get`](#get), the abstract mutables in
the template are replaced by their values:```js
R.equals(xyM.get(), xyA.get())
// true
```When written to, the abstract mutables in the template are written to with
matching elements from the written value:```js
xyM.view('x').set(3)
xL.get()
// 3
yL.get()
// 2
```The writes are performed [`holding`](#holding) event propagation.
It is considered an error, and the effect is unpredictable, if the written value
does not match the template, aside from the positions of abstract mutables, of
course, which means that write operations, [`set`](#set), [`remove`](#remove)
and [`modify`](#modify), on `Molecule`s and lensed atoms created from molecules
are only _partial_.Also, if the template contains multiple abstract mutables that correspond to the
same underlying state, then writing through the template will give unpredictable
results.## About
See [CHANGELOG](CHANGELOG.md).
### Implementation trade-offs
The implementations of the concepts provided by this library have been
**optimized for space** at a fairly low level. The good news is that you can
**use atoms and lensed atoms with impunity**. The bad news is that the
implementation is tightly bound to the internals of Kefir. Should the internals
change, this library will need to be updated as well.### Related work
The term "atom" is borrowed from [Clojure](http://clojure.org/reference/atoms)
and comes from the idea that one only performs
["atomic"](https://en.wikipedia.org/wiki/Read-modify-write), or
[race-condition](https://en.wikipedia.org/wiki/Race_condition) free, operations
on individual atoms.The idea of combining atoms and lenses came from
[Bacon.Model](https://github.com/baconjs/bacon.model), which we used initially.Our use of atoms was initially shaped by a search of way to make it possible to
program in ways similar to what could be done using
[Reagent](https://reagent-project.github.io/) and (early versions of)
[WebSharper UI.Next](http://websharper.com/docs/ui.next).