MCPcopy
hub / github.com/kriskowal/gtor

github.com/kriskowal/gtor @v0.0.1 sqlite

repository ↗ · DeepWiki ↗ · release v0.0.1 ↗
69 symbols 103 edges 24 files 1 documented · 1%
README

A General Theory of Reactivity

A work in progress.

In the context of a computer program, reactivity is the process of receiving external stimuli and propagating events. This is a rather broad definition that covers a wide variety of topics. The term is usually reserved for systems that respond in turns to sensors, schedules, and above all, problems that exist between the chair and keyboard.

The field of reactivity is carved into plots ranging from "reactive programming" to the subtly distinct "functional reactive programming", with acrage set aside for "self adjusting computation" and with neighbors like "bindings" and "operational transforms". Adherents favor everything from "continuation passing style" to "promises", or the related concepts of "deferreds" and "futures". Other problems lend themselves to "observables", "signals", or "behaviors", and everyone agrees that "streams" are a good idea, but "publishers" and "subscribers" are distinct.

In 1905, Einstein created a theory of special relativity that unified the concepts of space and time, and went on to incorporate gravity, to bring the three fundamentals of physical law into a single model. To a similar end, various minds in the field of reactivity have been converging on a model that unifies at least promises and observables.

         | **Singular**         | **Plural**

:----------: | :------------------: | :---------------------: Spatial | Value | Iterable<Value> Temporal | Promise<Value> | Observable<Value>

However, this description fails to capture all of the varigated concepts of reactivity. Rather, Rx conflates all reactive primitives into a single Observable type that can perform any role. Just as an array is an exemplar of an entire taxonomy of collections, promises, streams, and observables are merely representatives of their class of reactive primitives. As the common paraphrase of Einstein goes, everything should be made as simple as possible, but no simpler.

Concepts

For the purpose of discussion, we must establish a vocabulary. Some of these names have a long tradition, or at least some precedent in JavaScript. Some are disputed, borrowed, or fabricated.

A value is singular and spatial. It can be accessed or modified. If we break this atom, it will fall into two parts: the getter and the setter. Data flows in one direction, from the setter to the getter.

The duality of a getter and a setter, a producer and a consumer, or a writer and a reader, exists in every reactive primitive. Erik Meijer shows us the parallelism and reflection that exists between various types of reactive duals in his keynote for Lang.NEXT, 2014.

Singular is as opposed to plural or multiple. An array, or generally any collection, contains multiple values. An iterator is a plural getter. A generator and iterator form the plural dual for values in space.

Spatial is as opposed to temporal. Reactivity is about time.

A promise is a getter for a single value from the past or the future. In JavaScript, and in the language E from which we borrowed the concept, the corresponding setter is a resolver. Collectively, an asynchronous value is a deferred.

If a promise is the temporal analogue of a value, a stream is the temporal analogue of an array. The producer side of a stream is a writer and the consumer side is a reader. A reader is an asynchronous iterator and a writer is an asynchronous generator.

Interface
Value Value Singular Spatial
Getter Getter Singular Spatial
Setter Setter Singular Spatial
Array Value Plural Spatial
Iterator Getter Plural Spatial
Generator Setter Plural Spatial
Deferred Value Singular Temporal
Promise Getter Singular Temporal
Resolver Setter Singular Temporal
Stream Value Plural Temporal
Reader Getter Plural Temporal
Writer Setter Plural Temporal

Singular and temporal

An observer can subscribe to eventually see the value of a promise. They can do this before or after the promise has a value. Any number of observers can subscribe multiple times and any single observer can subscribe to the same promise multiple times.

As such, promises model dependency. Promises and resolvers can be safely distributed to any number of producers and consumers. If multiple producers race to resolve a promise, the experience of each producer is indistinguishable regardless of whether they won or lost the race. Likewise, if multiple consumers subscribe to a promise, the experience of each consumer is indistinguishable. One consumer cannot prevent another consumer from making progress. Information flows in one direction. Promises make reactive programs more robust and composable.

Promises are broadcast.

The law that no consumer can interfere with another consumer makes it impossible for promises to abort work in progress. A promise represents a result, not the work leading to that result.

A task has mostly the same form and features as a promise, but is unicast by default and can be cancelled. A task can have only one subscriber, but can be explicitly forked to create a new task that depends on the same result. Each subscriber can unsubscribe, and if all subscribers have unsubscribed and no further subscribers can be introduced, a task can abort its work.

Tasks are unicast and therefore cancelable.

See the accompanying sketch of a task implementation.

There is also an esoteric difference between a promise and a future. Promise resolvers accept either a value or a promise and will recursively unwrap transitive promises for promises. In most if not all strongly typed languages, this behavior makes it hard if not impossible to infer the type of a promise. A future is a promise’s strongly typed alter ego, which can take advantage of type inference to avoid having to explicitly cast the type of a promise.

Promises, tasks, and futures are three variations of a singular reactive value. They vary by being either broadcast or unicast, or by being suitable for strong or weak type systems.

Plural and temporal

There are many plural reactive value types. Each has a different adaptation for dealing with limited resources.

A stream has many of the same constraints as an array. Imagine a plane with space and time. If you rotate an array from the space axis to the time axis, it would become a stream. The order is important, and every value is significant.

Consumers and producers are unlikely to process values at the same rate. If the consumer is faster than the producer, it must idle between receiving values. If a producer is faster than their corresponding consumer, it must idle between sending values.

This pushing and pulling is captured by the concept of pressure. On the producer side, a vacuum stalls the consumer and a pressure sends values forward. On the consumer side, a vacuum draws values forward and pressure, often called back pressure, stalls the producer. Pressure exists to ensure that every value transits the setter to the getter.

Since the consumer of a stream expects to see every value, streams are unicast like tasks. As they are unicast they are also cancelable. Streams are a cooperation between the reader and the writer and information flows both ways. Data flows forward, acknowledgements flow backward, and either the consumer or producer can terminate the flow.

Although a stream is unicast, it is certainly possible to branch a stream into multiple streams in a variety of ways. A fork in a stream is an operator that ensures that every value gets sent to each of an array of consumers. The slowest of the forks determines the pressure, so the pressure of a fork can only be higher than that of a single consumer. The simpler strategy of providing a stream to multiple consumers produces a “round robin” load balancing effect, where each consumer receives an exclusive, possibly random, portion of the stream. The pressure of a shared stream can only be lower than that of a single consumer.

In the following example, the map operator creates two new streams from a single input stream. The slow map will see half as many values as the fast map. The slow map will consume and produce five values per second, and the fast map will consume and produce ten, sustaining a maximum throughput of fifteen values per second if the original stream can produce values that quickly. If the original stream can only produce ten or less values per second, the values will be distributed fairly between both consumers.

var slow = stream.map(function (n) {
    return Promise.return(n).delay(200);
});
var fast = stream.map(function (n) {
    return Promise.return(n).delay(100);
});

In contrast, publishers and subscribers are broadcast. Information flows only one direction, from the publishers to the subscribers. Also, there is no guarantee of continuity. The publisher does not wait for a subscriber and the subscriber receives whatever values were published during the period of their subscription. A stream would buffer all values produced until the consumer arrives.

With time series data, values that change over time but belie the same meaning, order and integrity may not be important. For example, if you were bombarded with weather forecasts, you could discard every report except the one you most recently received. Alternately, consider a value that represents the current time. Since the current time is always changing, it would not be meaningful, much less possible, to respond every moment it changes.

Time series data comes in two varieties: discrete and continuous. Discrete values should be pushed whereas continuous values should be pulled or polled. (If a homophone is a disaster, what are synonymous homophones?)

The current time or temperature are examples of continuous behaviors. Animation frames and morse code are examples of discrete signals.

Primitives

Let us consider each primitive in detail. Since the temporal primitives have spatial analogies, and since some of these spatial primitives are relatively new to JavaScript, we will review these first.

Iterators

An iterator is an object that allows us to lazily but synchronously consume multiple values. Iterators are not new to JavaScript, but there is a new standard forming at time of writing.

Iterators implement a next() method that returns an object that may have a value property, and may have a done property. Although the standard does not give this object a name, we will call it an iteration. If the iterator has produced the entirety of a sequence, the done property of the iteration will be true. Generator functions return iterators that expand on this basic definition. The value of a non-final iteration corresponds to a yield expression and the value of a done iteration corresponds to a return expression.

Iterators are an interface with many implementations. The canonical iterator yields the values from an array.

var iterator = iterate([1, 2, 3]);
var iteration = iterator.next();
expect(iteration.value).toBe(1);
iteration = iterator.next();
expect(iteration.value).toBe(2);
iteration = iterator.next();
expect(iteration.value).toBe(3);
iteration = iterator.next();
expect(iteration.done).toBe(true);

What distinguishes an iterator from an array is that it is lazy. An iterator does not necessarily end. We can have iterators for non-terminating sequences, like counting or the fibonacci sequence. The range function produces a sequence of values within an interval and separated by a stride.

function range(start, stop, step) {
    return {next: function () {
        var iteration;
        if (start < stop) {
            iteration = {value: start};
            start += step;
        } else {
            iteration = {done: true};
        }
        return iteration;
    }};
}

If the stop value of the range is Infinity, the iterator will have no end, and will never produce a done iteration. Unlike an array, an indefinite iterator consumes no more memory than an empty one.

var iterator = range(0, Infinity, 1);
expect(iterator.next().value).toBe(0);
expect(iterator.next().value).toBe(1);
expect(iterator.next().value).toBe(2);
// ...

The eager equivalent would produce an array, but would only work for bounded intervals since it must create an exhaustive collection in memory before returning.

function range(start, stop, step) {
    var result = [];
    while (start < stop) {
        result.push(start);
        start += step;
    }
    return result;
}

expect(range(0, 6, 2)).toEqual([0, 2, 4]);

Iterators may have alternate implementations of some methods familiar from arrays. For example, forEach would walk the iterator until it is exhausted. map would produce a new iterator of values passed through some transform, while filter would produce a new iterator of the values that pass a test. An iterator can support reduce, which would exhaust the iteration, but reduceRight would be less sensible since iterators only walk forward. Iterators may also have some methods that are unique to their character, like dropWhile and takeWhile.

We can save time and space by implementing pipelines with iterators instead of arrays. The following example can be interpre

Core symbols most depended-on inside this repo

Promise_getHandler
called by 17
promise.js
Promise_return
called by 14
promise.js
Promise_defer
called by 10
promise.js
Task_getHandler
called by 8
task.js
Promise_throw
called by 8
promise.js
Iterator
called by 7
iterator.js
isPromise
called by 5
promise.js
Promise_all
called by 4
promise.js

Shape

Function 69

Languages

TypeScript100%

Modules by API surface

promise.js25 symbols
iterator.js15 symbols
task.js8 symbols
byte-stream.js4 symbols
stream.js3 symbols
observable.js3 symbols
task-queue.js2 symbols
clock.js2 symbols
behavior.js2 symbols
scripts/generate.js1 symbols
promise-queue.js1 symbols
promise-machine.js1 symbols

Dependencies from manifests, versioned

asap1.0.0 · 1×
collections2.0.1 · 1×
docco0.6.3 · 1×
git-node-fs0.1.0 · 1×
jasminum2.0.5 · 1×
js-gitcreationix/js-git · 1×
q-git0.0.2 · 1×
q-io2.0.6 · 1×
weak-map1.0.5 · 1×

For agents

$ claude mcp add gtor \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact