calmm-js / estates

Estates is a library for first-class state and observable properties
MIT License
12 stars 0 forks source link
lenses observables properties state

Estates · Gitter GitHub stars npm

WIP

npm version Build Status Code Coverage

Contents

Tutorial

WIP

Reference

WIP

Related Work

WIP

Algorithm

The algorithm in Estates is basically:

transaction {
  perform subscriptions towards roots
    queue updates, completions, and side-effects
  perform updates towards leafs
    queue updates, completions, and side-effects
  perform completions towards leafs
    queue completions and side-effects
}
execute side-effects

All the phases within a transaction, namely subscribe, update, and complete, are performed completely before going to the next phase. Some operations can be performed without queuing, such as updating a property that only has a single source, but otherwise operations, specifically updates and completions, are queued so that they are performed at most once during a phase.

Transactions cannot be nested. An attempt to e.g. change the value of a property, triggering an update phase, during a transaction raises a fatal error. Potentially property changing side-effects, including user subscription callbacks and taps, are executed after the transaction so that they can trigger new transactions.

A disadvantage of having phases like this is that it will likely slow down processing. However, the main advantage of this approach is that the phases have simple semantics and the update phase guarantees glitch-free updates to applicative combinations of properties.

Motivation

Why do we need yet another library for observables?

Properties over event streams

When observables were popularized by Rx, the primary mode of use of observables was as streams of discrete events. Here is a characteristic example:

const count$ = plusOne$.merge(minusOne$).scan((x, y) => x + y, 0).startWith(0)

What is wrong with the above?

No, it is not that the count$ stream is "cold", although that is usually a mistake, too.

The root of the deeper problems, although not widely understood, is that the scan operation introduces state. Typically such state is local state and highly problematic for reasons discussed by David Barbour in Local State is Poison.

Discrete event streams, by their very nature, require you to deal with time. If the above count$ would be to track whether something has been selected, for example, it would be necessary to make sure that minusOne$ events are always preceded by plusOne$ events or the count$ could go negative.

On the whole, as discussed by David Barbour in Why Not Events, discrete event streams introduce a lot of accidental complexity in the form of having to meticulously handle timing considerations and having to laboriously gather all the pieces of local state introduced by streams to form views of the current state of a system.

A better approach is to avoid the use of event streams and state accumulators and replace them with applicative stateless combinations of properties derived from external state. Unfortunately most observable libraries are primarily designed to support programming with monadic event streams. Estates, however, is primarily designed to support programming with stateless applicative properties:

Dead code elimination

Most JavaScript observable libraries, with the notable exception of Most, today are not amenable to automatic dead code elimination performed by minification tools such as UglifyJS and bundling tools such as Rollup. The problem is that most observable libraries put all of their operations into the prototype chains of a few object constructors. To perform effective dead code elimination it would be necessary to perform whole program analysis to determine which methods cannot possibly be invoked. This is difficult enough that none of the contemporary tools perform such an analysis.

OTOH, when a library uses only free-standing functions and ES modules, it is possible to perform fairly effective dead code elimination simply by noting which functions are directly referenced. That is exactly what Estates does. All operations on properties are free-standing functions and unused operations can be dead code eliminated.

Performance

Many of the early observable implementations paid little or no attention to performance considerations. They use significantly more memory and CPU time than what is necessary. Then Most came out and showed that techniques such as stream fusion could also work in JavaScript. A goal for Estates is to minimize memory usage and enable partial fusion. Full fusion is likely to be more difficult to achieve in Estates, because glitch freedom requires delaying recomputations at join points in the dependency graph. Nevertheless, it should be possible to achieve significant performance advantages over previous implementations of observable properties in JavaScript.

One of the advantages of using only free-standing functions is that it is possible to avoid constructing new objects when dealing with constants. In Estates, constant values and nested objects and arrays possibly containing properties are also considered properties. In other words, there is no need for a constant combinator nor for a combineTemplate combinator. Also, combinators are optimized so that when given constants as inputs, they produce constants as outputs, if possible. This optimization can reduce memory usage significantly.

Note that at the moment this library is still very much in a drafting stage and does not implement all planned optimizations. Optimizations related to space usage are actually mostly there, but CPU time optimizations, notably allowing fusion of single source properties, has not yet been implemented.