≡ Estates ·
WIP
≡ 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?
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:
- All combinators produce properties that recall their last emitted value.
- All operations implicitly skip successive identical values.
- Propagation algorithm provides simple glitch freedom guarantees.
- Support for first-class decomposable and composable state is provided out of the box.
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.
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.