Giter Club home page Giter Club logo

hareactive's Introduction

Build Status codecov Gitter

Hareactive

Hareactive is a purely functional reactive programming (FRP) library for JavaScript and TypeScript. It is simple to use, powerful, and performant.

Key features

  • Simple and precise semantics. This means that everything in the library can be understood based on a very simple mental model. This makes the library easy to use and free from surprises.
  • Purely functional API.
  • Based on classic FRP. This means that the library makes a distinction between behaviors and streams.
  • Supports continuous time for expressive and efficient creation of time-dependent behavior.
  • Integrates with declarative side-effects in a way that is pure, testable and uses FRP for powerful handling of asynchronous operations.
  • Declarative testing. Hareactive programs are easy to test synchronously and declaratively.
  • Great performance.

Introduction

Hareactive is simple. It aims to have an API that is understandable and easy to use. It does that by making a clear distinction between semantics and implementation details. This means that the library implements a very simple mental model. By understanding this conceptual model the entire API can be understood.

This means that to you use Hareactive you do not have to worry about things such as "lazy observables", "hot vs cold observables" and "unicast vs multicast observables". These are all unfortunate concepts that confuse people and make reactive libraries harder to use. In Hareactive we consider such things implementation detail that users should never have to think about.

Hareactive implements what is called classic FRP. This means that it makes a distinction between two types of time dependent concepts. This makes code written in Hareactive more precise and easier to understand.

Hareactive is powerful. It features all the typical methods found in other FRP libraries. But on top of that it comes with many unique features that are rarely found elsewhere. For instance, continuous time.

Table of contents

Installation

Hareactive can be installed from npm. The package ships with both CommonJS modules and ES6 modules

npm install @funkia/hareactive

Conceptual overview

Hareactive contains four key concepts: Behavior, stream, future and now. This section will describe each of these at conceptual level.

For a practical introduction into using Hareactive see the tutorial. Unless you're already familiar with classic FRP you should at least read the sections on behavior, stream and now before you dive into the tutorial.

Behavior

A behavior is a value that changes over time. For instance, the current position of the mouse or the value of an input field is a behavior. Conceptually a behavior is a function from a point in time to a value. A behavior always has a value at any given time.

Since a behavior is a function of time we can visualize it by plotting it as a graph. The figure below shows two examples of behaviors. The left behavior is what we call a continuous behavior since it changes infinitely often. The right behavior only changes at specific moments, but it's still a function of time. Hareactive is implemented so that both types of behavior can be represented efficiently.

behavior figure

It is important to understand that behaviors are not implemented as functions. Although, in theory, they could be. All operations that Hareactive offers on behaviors can be explained and defined based on the understanding that a behavior is a function of time. It is a mental model that can be used to understand the library.

Stream

A Stream is a series of values that arrive over time. Conceptually it is a list of values where each value is associated with a moment in time.

An example could be a stream of keypresses that a user makes. Each keypress happens at a specific moment in time and with a value indicating which key was pressed.

Similarily to behaviors a stream can be visualized. But, in this case we wont get a graph. Instead we will get some points in time. Each point is called an occurrence. The value of an occurrence can be anything. For instance, the figure to the left may represent a stream of booleans where all the "low" stars represents an occurrence with the value false and the "high" stars represents true.

stream figure

The difference between a stream and a behavior is pretty clear when we see them visually. A behavior has a value at all points in time where a stream is a series of events that happens at specific moments in time.

To understand why Hareactive features both behavior and stream you may want to read the blog post Behaviors and streams, why both?.

Future

A future is a value associated with a certain point in time. For instance, the result of an HTTP-request is a future since it occurs at a specific time (when the response is received) and contains a value (the response itself).

Future has much in common with JavaScript's Promises. However, it is simpler. A future has no notion of resolution or rejection. That is, a specific future can be understood simply as a time and a value. Conceptually one can think of it as being implemented simply like this.

{time: 22, value: "Foo"}

The relationship between Future and Stream is the same as the relationship between having a variable of a type and a variable that is a list of that type. You wouldn't store a username as ["username"] because there is always exactly one username.

Similarly in Hareactive we don't use Stream to express the result of a HTTP-request since a HTTP-request only delivers a response exactly once. It is more precise to use a Future for things where there is exactly one occurrence and Stream where there may be zero or more.

Future, stream or behavior?

At first, the difference between the three things may be tricky to understand. Especially if you're used to other libraries where all three are represented as a single structure (maybe called "stream" or "observable"). The key is to understand that the three types represent things that are fundamentally different. And that expressing different things with different structures is beneficial.

You could forget about future and use a stream where you'd otherwise use a future. Because stream is more powerful than future. In the same way you could always use arrays of values instead of just single values. But you don't do that because username = "foo" expresses that only one username exists whereas username = ["foo"] gives the impression that a user can have more than one username. Similarly one could forget about numbers and just use strings instead. But saying amount = 22 is obviously better than amount = "22" because it's more precise.

This is how to figure out if a certain thing is a future, a stream or a behavior:

  1. Ask the question: "does the thing always have a current value?". If yes, you're done, the thing should be represented as a behavior.
  2. Ask the question: "does the thing happen exactly once?". If yes, the thing should be represented as a future. If no, you should use a stream.

Below are some examples:

  • The time remaining before an alarm goes off: The remaining time always have a current value, therefore it is a behavior.
  • The moment where the alarm goes off: This has no current value. And since the alarm only goes off a single time this is a future.
  • User clicking on a specific button: This has no notion of a current value. And the user may press the button more than once. Thus a stream is the proper representation.
  • Whether or not a button is currently pressed: This always has a current value. The button is always either pressed or not pressed. This should be represented as a behavior.
  • The tenth time a button is pressed: This happens once at a specific moment in time. Use a future.

Now

Now represents a computation that should be run in the present moment. Hence the name "now". Now is perhaps the most difficult concept in Hareactive.

A value of type Now is a description of something that we'd like to do. Such a description can declare that it wants to do one of two things.

  • Get the current value of behavior. This is done with the sample function. Since a Now-computation will always be run in the present it is impossible to sample a behavior in the past.
  • Describe side-effects. This is done with functions such as perform and performStream. With these functions we can describe things that should happen when a stream occurs.

Most Hareactive programs are bootstrapped by a Now-computation. That is, they take the form.

const main = ...

runNow(main);

Now is closely tied to the concept of stateful behaviors which is the topic of the next section.

How stateful behaviors work

A notorious problem in FRP is how to implement functions that return behaviors or streams that depend on the past. Such behaviors or streams are called "stateful"

For instance accumFrom creates a behavior that accumulates values over time. Clearly such a behavior depends on the past. Thus we say that accumFrom returns a stateful behavior.

Implementing stateful methods such as accumFrom in a way that is both intuitive to use, pure and memory safe is very tricky.

When implementing functions such as accumFrom most reactive libraries in JavaScript do one of these two things:

  • Calling accumFrom doesn't begin accumulating state at all. Only when someone starts observing the result of accumFrom is state accumulated. This is very counter intuitive behavior.
  • Calling accumFrom starts accumulating state from when accumFrom is called. This is pretty easy to understand. But it makes accumFrom impure as it will not return the same behavior when called at different time.

To solve this problem Hareactive uses a solution invented by Atze van der Ploeg and presented in his paper "Principled Practical FRP". His brilliant idea gives Hareactive the best of both worlds. Intuitive behavior and purity.

The solution means that some functions return a value that, compared to what one might expect, is wrapped in an "extra" behavior. This "behavior wrapping" is applied to all functions that return a result that depends on the past. The before mentioned accumFrom, for instance, returns a value of type Behavior<Behavior<A>>.

Remember that a behavior is a value that depends on time. It is a function from time. Therefore a behavior of a behavior is like a value that depends on two moments in time. This makes sense for accumFrom because the result of accumulating depends both on when we start accumulating and where we are now.

To get rid of the extra layer of nesting we often use sample. The sample function returns a Now-computation that asks for the current value of a behavior. It has the type (b: Behavior<A>) => Now<A>. Using sample with accumFrom looks like this.

const count = sample(accumFrom((acc, inc) => acc + inc, 0, incrementStream));

Here count has type Now<Behavior<A>> and it represents a Now-computation that will start accumulating from the present moment.

Flattening nested FRP values

The definition of higher-order FRP is that it allows for FRP primitives nested inside other FRP primitives. Combinations like streams of streams, behaviors of behaviors, streams of futures, and any others are possible.

The benefit of higher-order FRP is increased expressiveness that makes it possibe to express many real-world scenarios with ease. One example would be an application with a list of counters. Each counter has a value which can be represented as a Behavior<number>. A list of counters would then have the type Array<Behavior<number>>. If additionally the list itself can change (maybe new counters can be added) then the type whould be Behavior<Array<Behavior<number>>. This higher-order type nicely captures that we have a changing list of changing numbers.

The downside of higher-order FRP is that sometimes dealing with these nested types can be tricky. Hareactive provides a number of functions to help with this. The table below gives an overview.

Outer Inner Function
Behavior anything sample (when inside Now)
Behavior Behavior flat
Behavior Stream shiftCurrent
Stream Behavior switcher, selfie
Stream Stream shift
Stream Future n/a
Future Behavior switchTo

Tutorial/cookbook

This cookbook will demonstrate how to use Hareactive. The examples gradually increase in complexity. Reading from the top serves as an tutorial about the library.

Please open an issue if anything is unclear from the explanations given.

General

How do I apply a function to the value inside a behavior?

You can use the map method. For instance, if you have a behavior of a number you can square the number as follows. map returns a new behavior with all values of the original behavior passed through the function:

behaviorOfNumber.map((n) => n * n);

map is also available as a function instead of a method.

map((n) => n * n, behaviorOfNumber);

Can I also apply a function to the occurrences in a stream?

Yes. Streams also have a map method.

streamOfNumbers.map((n) => n * n);

The map function also works with streams.

map((n) => n * n, streamOfNumbers);

If I have two streams how can I merge them into one with the occurrences from both?

This is done with the combine method or the combine function.

combine(firstStream, secondStream);

You can similarly combine any number of streams:

combine(firstStream, secondStream, thirdStream, etcStream);

How do I combine two behaviors?

Behaviors always have a current value. So to combine them you will have to specify how to turn the two values from the two behaviors into a single value. You do that with the lift function.

For instance, if you have two behaviors of numbers you can combine them by adding their values together.

lift((n, m) => n + m, behaviorN, behaviorM);

You can also combine in this fashion any number of behaviors, which has to match the number of the function arguments:

lift((n, m, q) => (n + m) / q, behaviorN, behaviorM, behaviorQ);

How do I turn a stream into a behavior?

You probably want stepperFrom:

const b = stepperFrom(initial, stream);

Creating behaviors and streams

Can I create a stream from events on a DOM element?

We've though of that. Hareactive comes with a function for doing just that:

streamFromEvent(domElement, "click");

Can I turn an item in localStorage into a behavior?

Definitely. Yes. fromFunction takes an impure function and turns it into a behavior whose value at any time is equal to what the impure function would return at that time:

const localStorageBehavior = fromFunction(() => localStorage.getItem("foobar"));

Debugging

My program isn't working. Is there an easy way to check what is going on in my behaviors or streams?

Both streams and behaviors have a log method that logs to the console when something happens.

misbehavingStream.log();

API

Future

Future.of<A>(a: A): Future<A>

Converts any value into a future that has "always occurred". Semantically Future.of(a) is equivalent to (-Infinity, a).

fromPromise<A>(p: Promise<A>): Future<A>

Converts a promise to a future.

isFuture(f: any): f is Future<any>

Returns true if f is a future and false otherwise.

Future#listen<A>(o: Consumer<A>): void

Adds a consumer as listener to a future. If the future has already occurred the consumer is immediately pushed to.

Stream

empty: Stream<any>

Empty stream.

Stream.of<A>(a: A): Stream<A>

This function does not exist. Use empty to create a dummy stream for testing purposes.

isStream(s: any): s is Stream<any>

Returns true if s is a stream and false otherwise.

apply<A, B>(behavior: Behavior<(a: A) => B>, stream: Stream<A>): Stream<B>

Applies a function-valued behavior to a stream. Whenever the stream has an occurrence the value is passed through the current function of the behavior.

filter<A>(predicate: (a: A) => boolean, s: Stream<A>): Stream<A>

Returns a stream with all the occurrences from s for which predicate returns true.

const stream = testStreamFromArray([1, 3, 2, 4, 1]);
const filtered = stream.filter((n) => n > 2);
filtered.semantic(); //=> [{ time: 1, value: 3 }, { time: 3, value: 4 }]

split<A>(predicate: (a: A) => boolean, stream: Stream<A>): [Stream<A>, Stream<A>]

Returns a pair of streams. The first contains all occurrences from stream for which predicate returns true and the other the occurrences for which predicate returns false.

const whereTrue = stream.filter(predicate);
const whereFalse = stream.filter((v) => !predicate(v));
// is equivalent to
const [whereTrue, whereFalse] = split(predicate, stream);

filterApply<A>(predicate: Behavior<(a: A) => boolean>, stream: Stream<A>): Stream<A>

Filters a stream by applying the predicate-valued behavior to all occurrences.

keepWhen<A>(stream: Stream<A>, behavior: Behavior<boolean>): Stream<A>

Whenever stream has an occurrence the current value of behavior is considered. If it is true then the returned stream also has the occurrence—otherwise it doesn't. The behavior works as a filter that decides whether or not values are let through.

scanFrom<A, B>(fn: (a: A, b: B) => B, startingValue: B, stream: Stream<A>): Behavior<Stream<B>>

A stateful scan.

snapshot<B>(b: Behavior<B>, s: Stream<any>): Stream<B>

Creates a stream that occurs exactly when s occurs. Every time the stream s has an occurrence the current value of b is sampled. The value in the occurrence is then replaced with the sampled value.

const stream = testStreamFromObject({
  1: 0,
  4: 0,
  8: 0,
  12: 0
});
const shot = snapshot(time, stream);
const result = testStreamFromObject({
  1: 1,
  4: 4,
  8: 8,
  12: 12
});
// short == result

snapshotWith<A, B, C>(f: (a: A, b: B) => C, b: Behavior<B>, s: Stream<A>): Stream<C>

Returns a stream that occurs whenever s occurs. At each occurrence the value from s and the value from b is passed to f and the return value is the value of the returned streams occurrence.

shiftCurrent<A>(b: Behavior<Stream<A>>): Stream<A>

Takes a stream valued behavior and returns a stream that emits values from the current stream at the behavior. I.e. the returned stream always "shifts" to the current stream at the behavior.

shift

function shift<A>(s: Stream<Stream<A>>): Now<Stream<A>>;

Takes a stream of a stream and returns a stream that emits from the last stream.

shiftFrom

function shiftFrom<A>(s: Stream<Stream<A>>): Behavior<Stream<A>>;

Takes a stream of a stream and returns a stream that emits from the last stream.

changes

changes<A>(b: Behavior<A>, comparator: (v: A, u: A) => boolean = (v, u) => v === u): Stream<A>;

Takes a behavior and returns a stream that has an occurrence whenever the behaviors value changes.

The second argument is an optional comparator that will be used to determine equality between values of the behavior. It defaults to using ===. This default is only intended to be used for JavaScript primitives like booleans, numbers, strings, etc.

combine<A, B>(a: Stream<A>, b: Stream<B>): Stream<(A|B)>

Combines two streams into a single stream that contains the occurrences of both a and b sorted by the time of their occurrences. If two occurrences happens at the exactly same time then the occurrence from a comes first.

const s1 = testStreamFromObject({ 0: "#1", 2: "#3" });
const s2 = testStreamFromObject({ 1: "#2", 2: "#4", 3: "#5" });
const combined = combine(s1, s2);
assert.deepEqual(combined.semantic(), [
  { time: 0, value: "#1" },
  { time: 1, value: "#2" },
  { time: 2, value: "#3" },
  { time: 2, value: "#4" },
  { time: 3, value: "#5" }
]);

isStream(obj: any): boolean

Returns true if obj is a stream and false otherwise.

isStream(empty); //=> true
isStream(12); //=> false

delay<A>(ms: number, s: Stream<A>): Stream<A>

Returns a stream that occurs ms milliseconds after s occurs.

throttle<A>(ms: number, s: Stream<A>): Stream<A>

Returns a stream that after occurring, ignores the next occurrences in ms milliseconds.

stream.log(prefix?: string)

The log method on streams logs the value of every occurrence using console.log. It is intended to be used for debugging streams during development.

The option prefix argument will be logged along with every value if specified.

myStream.log("myStream:");

Behavior

Behavior.of<A>(a: A): Behavior<A>

Converts any value into a constant behavior.

fromFunction<B>(fn: () => B): Behavior<B>

This takes an impure function that varies over time and returns a pull-driven behavior. This is particularly useful if the function is contionusly changing, like Date.now.

isBehavior(b: any): b is Behavior<any>

Returns true if b is a behavior and false otherwise.

whenFrom(b: Behavior<boolean>): Behavior<Future<{}>>

Takes a boolean valued behavior an returns a behavior that at any point in time contains a future that occurs in the next moment where b is true.

snapshot<A>(b: Behavior<A>, f: Future<any>): Behavior<Future<A>>

Creates a future than on occurence samples the current value of the behavior and occurs with that value. That is, the original value of the future is overwritten with the behavior value at the time when the future occurs.

stepTo<A>(init: A, next: Future<A>): Behavior<A>

From an initial value and a future value, stepTo creates a new behavior that has the initial value until next occurs, after which it has the value of the future.

switchTo<A>(init: Behavior<A>, next: Future<Behavior<A>>): Behavior<A>

Creates a new behavior that acts exactly like initial until next occurs after which it acts like the behavior it contains.

switcher<A>(init: Behavior<A>, s: Stream<Behavior<A>>): Now<Behavior<A>>

A behavior of a behavior that switches to the latest behavior from s.

switcherFrom<A>(init: Behavior<A>, s: Stream<Behavior<A>>): Behavior<Behavior<A>>

A behavior of a behavior that switches to the latest behavior from s.

stepperFrom<B>(initial: B, steps: Stream<B>): Behavior<Behavior<B>>

Creates a behavior whose value is the last occurrence in the stream.

scanFrom<A, B>(fn: (a: A, b: B) => B, init: B, source: Stream<A>): Behavior<Behavior<B>>

The returned behavior initially has the initial value, on each occurrence in source the function is applied to the current value of the behaviour and the value of the occurrence, the returned value becomes the next value of the behavior.

moment<A>(f: (sample: <B>(b: Behavior<B>) => B) => A): Behavior<A>

Constructs a behavior based on a function. At any point in time the value of the behavior is equal to the result of applying the function to a sampling function. The sampling function returns the current value of any behavior.

moment is a powerful function that can do many things and sometimes it can do them in a way that is a lot easier than other functions. A typical usage of moment has the following form.

moment((at) => {
  ...
})

Above, the at function above can be applied to any behavior and it will return the current value of the behavior. The following example adds together the values of three behaviors of numbers.

const sum = moment((at) => at(aBeh) + at(bBeh) + at(cBeh));

The above could also be achieved with lift. However, moment can give better performance when used with a function which dynamically switches which behaviors it depends on. To understand this, consider the following contrived example.

const lifted = lift((a, b, c, d) => (a && b ? c : d), aB, bB, cB, dB);

Here the resulting behavior will always depend on both aB, bB, cB, dB. This means that if any of them changes then the value of lifted will be recomputed. But, if for instance, aB is false then the function actually only uses aB and there is no need to recompute lifted if any of the other behaviors changes. However, lift can't know this since the function given to it is just a "black box".

If, on the other hand, we use moment:

const momented = moment((at) => (at(aB) && at(bB) ? at(cB) : at(dB)));

Then moment can simply check which behaviors are actually sampled inside the function passed to it, and it uses this information to figure out which behaviors momented depends upon in any given time. This means that when aB is false the implementation can figure out that, currently, momented only depends on atB and there is no need to recompute momented when any of the other behaviors changes.

moment can also be very useful with behaviors nested inside behaviors. If persons is a behavior of an array of persons and is of the type Behavior<{ age: Behavior<number>, name: string }[]> then the following code creates a behavior that at any time is equal to the name of the first person in the array whose age is greater than 20.

const first = moment((at) => {
  for (const person of at(persons)) {
    if (at(person.age) > 20) {
      return person.name;
    }
  }
});

Achieving something similar without moment would be quite tricky.

time: Behavior<Time>

A behavior whose value is the number of milliseconds elapsed since UNIX epoch. I.e. its current value is equal to the value got by calling Date.now.

measureTime: Now<Behavior<Time>>

The now-computation results in a behavior that tracks the time passed since its creation.

measureTimeFrom: Behavior<Behavior<Time>>

A behavior giving access to continuous time. When sampled the outer behavior gives a behavior with values that contain the difference between the current sample time and the time at which the outer behavior was sampled.

integrate(behavior: Behavior<number>): Behavior<Behavior<number>>

Integrate behavior with respect to time.

The value of the behavior is treated as a rate of change per millisecond.

integrateFrom(behavior: Behavior<number>): Behavior<Behavior<number>>

Integrate behavior with respect to time.

The value of the behavior is treated as a rate of change per millisecond.

behavior.log(prefix?: string, ms: number = 100)

The log method on behaviors logs the value of the behavior whenever it changes using console.log. It is intended to be used for debugging behaviors during development.

If the behavior is a pull behavior (i.e. it may change infinitely often) then changes will only be logged every ms milliseconds.

The option prefix argument will be logged along with every value if specified.

myBehavior.log("myBehavior:");
time.map(t => t * t).log("Time squared is:", 1000);

Now

The Now monad represents a computation that takes place in a given moment and where the moment will always be now when the computation is run.

Now.of<A>(a: A): Now<A>

Converts any value into the Now monad.

async<A>(comp: IO<A>): Now<Future<A>>

Run an asynchronous IO action and return a future in the Now monad that resolves with the eventual result of the IO action once it completes. This function is what allows the Now monad to execute imperative actions in a way that is pure and integrated with FRP.

sample<A>(b: Behavior<A>): Now<A>

Returns the current value of a behavior in the Now monad. This is possible because computations in the Now monad have an associated point in time.

performStream<A>(s: Stream<IO<A>>): Now<Stream<A>>

Takes a stream of IO actions and return a stream in a now computation. When run the now computation executes each IO action and delivers their result into the created stream.

performStreamLatest<A>(s: Stream<IO<A>>): Now<Stream<A>>

A variant of performStream where outdated IO results are ignored.

performStreamOrdered<A>(s: Stream<IO<A>>): Now<Stream<A>>

A variant of performStream where IO results occur in the same order.

plan<A>(future: Future<Now<A>>): Now<Future<A>>

Convert a future now computation into a now computation of a future. This function is what allows a Now-computation to reach beyond the current moment that it is running in.

runNow<A>(now: Now<Future<A>>): Promise<A>

Run the given Now-computation. The returned promise resolves once the future that is the result of running the now computation occurs. This is an impure function and should not be used in normal application code.

Contributing

Contributions are very welcome. Development happens as follows:

Install dependencies.

npm install

Run tests.

npm test

Running the tests will generate an HTML coverage report in ./coverage/.

Continuously run the tests with

npm run test-watch

We also use tslint for ensuring a coherent code-style.

Benchmark

Get set up to running the benchmarks:

npm run build
./benchmark/prepare-benchmarks.sh

Run all benchmarks with:

npm run bench

Run a single benchmark with:

node benchmark/<name-of-benchmark>

For example

node benchmark/scan.suite

hareactive's People

Contributors

alidd avatar andrewraycode avatar arlair avatar carloslfu avatar dependabot[bot] avatar dmitriz avatar fbn avatar jomik avatar jopie64 avatar limemloh avatar paldepind avatar raveclassic avatar stevekrouse avatar tehshrike avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hareactive's Issues

Examples?

As a newcomer to FRP topics, I tried to spin up a toy project gluing this library to the UI, but have struggled to get off the ground (say, with a counter) because there aren't quickstart examples in this project I can find

FP newbie question about library implementation

I'm a newbie to FP (with a background 10+ years in OOP). Researching the various library/framework available I have approaded to this repository which I find really interesting and divulgative. This library and Turbine looks like a more interesting approach than cycle js for example.

The question is about library implementation.

I see you have user class and hereditariaty to implement your data structures with abbondand use of the 'this' keyword. So I suppose the library is developed in OOP, or I mistaking something?

If yes is not a controsense to implement a FP library using OOP?

Thanks in advance for the explanation and compliments for the great work.

Approach to glitches

When a node (A) occurs multiple times as a dependency of another node (B), B will fire many times for each firing of A, and some of the firings will have wrong value.

The following test case illustrates the problem:

    it("handles diamond dependencies without glitches", () => {
      const source = sinkBehavior(0);
      const b = lift((a, b) => a + b, source, source);
      const spy = subscribeSpy(b);
      assert.deepEqual(at(b), 0);
      source.push(1);
      assert.deepEqual(at(b), 2);
      assert.deepEqual(spy.args, [[0], [2]]);
    });

It fails with:

  1) behavior
       applicative
         handles diamond dependencies without glitches:

      AssertionError: expected [ [ 0 ], [ 1 ], [ 2 ] ] to deeply equal [ [ 0 ], [ 2 ] ]

The presence of value 1 indicates that the framework let the application observe inconsistent values for source (0 and 1 in this case).

Is this the expected behavior? If not, are there plans to fix it?

The Scan confusion

The scan function seems to be both common and useful,
but it also caused me some confusions like here and here that I'd like to clear if possible.

Here is my current understanding (appreciate any correction):

  • The scan function in flyd is impure, strictly speaking. The simplest possible example is
const s = flyd.stream()
const s1 = flyd.scan(`add`, 1, s)
s1() //=> 1
s(1)
const s2 = flyd.scan(`add`, 1, s)
s2() //=> 2

So the result depends on the time of calling the scan.

  • I would like to run the same example with Hareactive, but at the moment it is not quite clear to me what would be the best way. From the scan tests I see that a sinkStream is used to create a "ProducerStream", then scan is followed the .at() evaluation, and then by subscribe, whose role is not quite clear to me, in particular, whether it turns stream from pending into active, like other libraries do. Then events need to be published. I wonder if there were any more direct way similar to the above.

  • It can be seen as composition of truncating the stream events between two moments into array (forgetting the times) and subsequent reduce. The latter part is pure. The impurity comes from the implicit dependence on the first moment (the moment when the scan was called). It is still pure in the second moment, which is the current time.

  • The scan becomes pure when applied to the so-called "cold" (I find "pending" more descriptive) streams, the ones that had not received any value yet. This is how they do it in MostJS. Any of their method "activating" the stream, transforms it into Promise, after which methods like scan are not allowed. That way scan remains pure.

  • Applying scan only to the pending streams is the most common use case, as e.g. @JAForbes was suggesting in MithrilJS/mithril.js#1822 . Such as the action stream is passed to scan before at the initialisation time, whereas the actions begin to arrive after. This fact is also confirmed by the absence of tests in the non-pending cases, for instance, note how in https://github.com/paldepind/flyd/blob/master/test/index.js#L426 the stream is always created empty.

  • The 2 scan methods here are pure, however, they differ from other libraries in which they return the more complex types of either Behavior<Stream<A>> or Behavior<Behavior<A>>.

The implementation (as always) varies among libraries:

  • flyd (and mithril/stream) allows scan on any stream and returns stream

  • MostJS scan regards all Streams as "cold", with the additional mosj-subject to use with active streams, however, the purity is lost in that case.

  • Xstream does not have scan, it seems to be replaced with the fold which "Combines events from the past throughout the entire execution of the input stream". That seems to solve the purity problem for them, but may not be as convenient to use.

  • KefirJS https://rpominov.github.io/kefir/#scan and BaconJS https://baconjs.github.io/api.html let their scan to transform stream into
    what they call "property", which I understand is the same as Behavior here.
    I am not familiar with details but they seem to talk about "active", so possibly they way is similar to MostJS.

  • The RxJS makes the seed argument optional. Which, however, presents problems if it is of different type than the accumulator. (They only demonstrate the simple addition case, where the types are equal.)
    The same is in KefirJS

Possible suggestions:

  • Change the name to something like pureScan to emphasise both the difference and the motivation for the additional complexity, and to avoid the confusion with the other scans.

  • I would like to have some safe and simple variant. Like stream and behavior in one thing, what Xstream calls the Memory Stream. So I know that both stream and behavior would conform to this memoryStream interface and I don't have to think which one is which. It may be derived from other more refined versions, but I would like to have it for the sake of convenience.

  • A new MemoryStream interface might be a great entry point to build adapters for other libraries as it would accept all streams, promises, behaviours, properties and even observables. So people can use their old code with the api they understand, which is great.

  • A new Pending interface could be combined with Streams, Behaviors, or MemoryStreams. It would allow the use the "unlifted" version of scan, while preserving the purity.

  • The scan function for the Pending interface could be called something like "pendingScan" to emphasise its use case. It would only apply to pending memory streams, in its pure form. Its impure brother can be called "impureScan" and would apply to any memory stream, but with "impure" in it's name, it is no more the library's responsibility :)

  • The reducer would gets called and the initialisation time (when the stream is not pending) and when the events are emitted.

Let me know what you think.

Speciel do-notation for view creation

Currently in examples/simple/index.ts the view function looks like this:

view({validB, lengthB}) {
  return Do(function*(): Iterator<Component<any>> {
    yield span("Please enter an email address:");
    const {inputValue: emailB} = yield input();
    yield br();
    yield text("The length of the email is ");
    yield text(lengthB);
    yield br();
    yield text("The address is ");
    yield text(validB.map(t => t ? "valid" : "invalid"));
    return Component.of({behaviors: [emailB], events: []});
  });
}

This uses the general do-notation form Jabz. Maybe it is worth it to create a modified overloaded do-notation for creating views. It could allow for

  • yielding string and string valued behaviors directly instead of using the text function.
  • yielding several components at once by wrapping them in array. This of course cannot be used when the return value of the component is needed, like with input in the example.
  • Not needing to wrap the final return value with Component.of.
view({validB, lengthB}) {
  return componentDo(function*(): Iterator<Component<any>> {
    yield span("Please enter an email address:");
    const {inputValue: emailB} = yield input();
    yield [
      br(),
      "The length of the email is ",
      text(lengthB),
      br(),
      "The address is ",
      validB.map(t => t ? "valid" : "invalid"),
    ];
    return {behaviors: [emailB], events: []};
  });
}

The downside is of course that complexity is increased. Additionally it might be harder to understand due to the extra magic that is going on.

Animation tools

I would be nice to have some tools to help with describing transitions with behaviors. I imagine an API like this:

const config = {
  duration: 5,
  timingFunction: linear,
  delay: 0
}
const t = transitionBehavior(config, numberStream, initialValue);

numberStream is telling which values the resulting behavior should make transitions to.

This together with a collection of timing functions would be a good start.

How to model behaviors that can only be sampled asynchronously

Consider a "behavior" such as the state of some data on a remote system (perhaps accessible via a REST API). The state exists at all points in time, but in order to access it locally I need to communicate asynchronously with the remote machine over the network.

In something like rxjs, I don't really have a choice: everything must be modeled as a stream. I'd probably do something like const req = new Subject(); const responses = req.flatMap(_ => updateState()).publish(), subscribe my response handling to responses, and do req.next() whenever I want to update the remote state. Or perhaps something simpler, like Observable.interval(1000).flatMap(_ => updateState()), although this is worse for granularity.

So in hareactive, do I model this kind of thing as a Behavior<Future<TState>> or something?

Add loopNow

We need a loopNow function to establish circular dependencies in a now computation. Similar to what loop in Turbine does for Component.

feedback

Hi @paldepind. I opened this issue as a feedback to your mail. Sorry for the delay.

I'm going here to share some conclusions on my earlier experiments and my readings (I'd be happy to take part on this but unfortunately I cant find more time to work on another project actually. But I'll be glad to discuss any issue with you on this repo. I could also contribute later If I find the time)

I think it's important to define a denotational model before starting the implementation ( As Conal mentioned there are 2 fundamental requirements to define an FRP system: Precise denotation and Continuous time.). It's not required to be a formal model with denotational semantics. One can do it simply with a pure language (Haskell) or even with a pure subset of JS to TypeScript. For example take a look at reactive banana model.

IMO, it'd be better to take the semantic model described in this paper (the model is described in the first 4 pages) instead of the Classic model. It uses standard type classes (Functors, Applicatives, ...). It's not necessary to follow the implementation defined in the rest of the paper.

As for the implementation, In my experiments I was mainly focused on reactive behaviors. Traditionally (in non pure languages like JS, Java or Scala) they are implemented on top of Event Emitters & dependency graphs but as you mentioned coming up with a memory-leak free implementation is quite challenging without native weak maps. in my cfrp lib (which TBH is really far from the classic FRP) I distinguished 2 types of behaviors

  • root behaviors are connected to events directly
  • computed behaviors are expressed using root and possibly other computed behaviors, but they are not directly connected to any event

Computed behaviors can be evaluated lazily, since they entirely depend on their sources.

Root behaviors, OTOH, are eager by nature, they must be 'updated' on each event occurrence. We can't evaluate them lazily unless we keep the entire event history since the last evaluation (which may cause serious space/time leaks, this was the issue with my pull based implementation unReact). The issue is to prevent memory leaks, especially with dynamic behaviors: A behavior will typically subscribe to its source events, but how it should unsubcribe from them?

There is another interesting alternative to traditional dependency graphs and it uses pure FP concepts to build a glitch free dependency model. The solution was presented by Conal Elliott in this paper. Basically it defined a Source of value as a couple of Extractor (to extract a value) and a Notifier (to notify changes)

type Extractor a  = IO a
type Notify = IO ()  IO ()

type Source a = (Notify, Extractor a)

Then define computation on Source values using Functors & Applicatives. I found this solution more elegant and also simpler than traditional dependency graphs

i made a rough JS port (abstracting over IOs). The solution was part of Conal's attempt to build a push/pull implementation of FRP. But was eschewed in favor of an IO-free implementation (sort of, it used blocking threads to implement events on top of Futures). AFAIK this was the last Conal's implementation.

Testing

Testing FRP code can sometimes be tricky and cumbersome.

To solve the problem we are working on a way to FRP code in a way that is declarative and convenient.

Here is a simple example. Let's say one want's to test this function:

function foobar(stream1, stream2) {
  const a = stream1.filter(isEven).map((n) => n * n);
  const b = stream1.filter((n) => !isEven(n)).map(Math.sqrt);
  return combine(a, b);
}

Currently, such a function can be tested like this:

const a = testStreamFromObject({ 0: 1, 2: 4, 4: 6 });
const b = testStreamFromObject({ 1: 9, 3: 8 });
const result = foobar(a, b);
const expected = testStreamFromObject({ 1: 3, 2: 16, 4: 36 });
assert.deepEqual(result.semantic(), expected.semantic());

The testing feature is currently quite incomplete. And we completely lack a way to test code that uses Now. I think testing Now could be achieved by combining elements from how testing IO works and how testing behavior/streams work.

This issue is for discussion ideas and problems related to testing.

Better type safety possible for accumCombine?

I noticed while looking at TodoMVC that the following isn't fully typesafe,

  return accumCombine(
    [
      [prependItemS, (item, list) => [item].concat(list)],
      [
        removeKeyListS,
        (keys, list) => list.filter((item) => !includes(itemToKey(item), keys))
      ]
    ],
    initial
  );

where prependItemS: Stream<A> and removeKeyListS: Stream<B[]>, but item: any and keys: any.

I looked at the types,

export declare function accumFrom<A, B>(f: (a: A, b: B) => B, initial: B, source: Stream<A>): Behavior<Behavior<B>>;
export declare function accum<A, B>(f: (a: A, b: B) => B, initial: B, source: Stream<A>): Now<Behavior<B>>;
export declare type AccumPair<A> = [Stream<any>, (a: any, b: A) => A];
export declare function accumCombineFrom<B>(pairs: AccumPair<B>[], initial: B): Behavior<Behavior<B>>;
export declare function accumCombine<B>(pairs: AccumPair<B>[], initial: B): Now<Behavior<B>>;

I see that if you do this

export declare type AccumPair<A, C> = [Stream<C>, (a: C, b: A) => A];

It won't work because C will get bound once to the first element of the first element of pairs.

Is rank-n polymorphism what is needed here? I've read about it a bit. Does TS support it?

Running tests in browsers

We should figure out how to run tests across browsers. The solution should be compatible with some CI service.

Improve performance when stream has a single child

Small comment regarding b496b69.

Previously Stream had an array of listeners and the methods push and a publish. push contained the logic of the stream and would be implemented differently for each stream type. push had to call publish with a value and publish would then deliver this value to all of a the streams children.

This meant that in a chain of streams like stream.filter(pred).map(fn).scan(acc).subscribe(impFn) if a value was pushed to stream it would go throw this chain of function calls before reaching impFn:
publish -> push (filter) -> publish (filter) -> push (map) -> publish (map) -> push (scan) -> publish (scan) -> push (subscribe)
Even though all of the function call was in-lined it reduced performance. After b496b69 the above chain is shortened to:
push -> push (filter) -> push (map) -> push (scan) -> push (subscribe)
I.e. publish has been eliminated.

It works pretty much like this: each stream assumes that it has only one child as a child property. That is push can just call this.child.push to deliver its result directly to its child. This works fine as long as a stream actually only has a single child. When a stream has more than one child a special MultiConsumer is installed instead of an actual child. MultiConsumer does what publish did before, i.e. delivering a value to a list of children. In this way we can get rid of publish when it is irrelevant and seamlessly get something equivalent back whenever a stream actually has several listeners.

The performance improvements are decent:

------------------------- filter-map-scan -------------------------
Stream old                       71.27 op/s ±  0.98%   (81 samples)
Stream                          106.01 op/s ±  2.37%   (80 samples)
most                            108.82 op/s ±  1.30%   (82 samples)
---------------------------- Best: most ----------------------------


------------------------ map-map-map-stream ------------------------
Stream old                      228.08 op/s ±  0.77%   (86 samples)
Stream                          256.93 op/s ±  0.32%   (83 samples)
most                            274.56 op/s ±  0.57%   (83 samples)
---------------------------- Best: most ----------------------------


--------------------------- merge-stream ---------------------------
Stream old                       30.43 op/s ±  0.42%   (71 samples)
Stream                           38.95 op/s ±  0.30%   (87 samples)
--------------------------- Best: Stream ---------------------------


--------------------------- scan-stream ---------------------------
Stream old                      299.44 op/s ±  0.72%   (85 samples)
Stream                          328.17 op/s ±  0.78%   (87 samples)
most                            455.17 op/s ±  1.31%   (86 samples)
---------------------------- Best: most ----------------------------

The sample confusion

I conceptually understand how sample solves the hot and cold observable problem, but I don't have a strong in-my-bones intuition for it yet. Part of my confusion is that I don't see any alternative for "the first moment in time" of when to start accumulating. Isn't it always "when the code runs"? When would we want a delayed accumulation time?

What might solve this confusion for me is to show how sample allows us to model both hot and cold observables.

How To Cite Hareactive?

We are using Hareactive in one of our projects and would like to cite it in a paper. Is there a preffered way by the core contributors to do so?

Improve documentation

The documentation is currently pretty bad.

Add

  • Description of Stream
  • Description of Behavior
  • Description of Now
  • Better explanations
  • Examples

Source maps in tests

In tests line numbers are currently referring to the compiled JavaScript.

That is quite annoying 😄

How to accomplish `zip([ streamA, streamB ])` with hareactive?

Given the scenario that occurrences are pushed from the outside world, such as incoming websocket messages, and needing to reply only when each of N streams have emitted since last reply, using the values of the streams to form the reply, how can this be accomplished with hareactive?

In some libraries, this is as simple as zip([ streamA, streamB ]), but it isn't clear to me how to do it with the behavior mentality. Making behaviors of A and B and lifting them to form the reply is close, but it disregards the timing/state problem, which remains by far the hardest part of the whole scenario.

Here's a psuedo code example:

const messageA$ = Stream()
const messageB$ = Stream()
wsA.on('message', messageA$.emit)
wsB.on('message', messageB$.emit)

const reply$ = zip([ messageA$, messageB$ ]).map(makeTheReply)
reply$.on(reply => [ wsA, wsB ].forEach(ws => ws.emit('reply', reply)))

`changes` and push/pull

As noted in #21 changes doesn't work with pull behaviors. Instead it causes a runtime error. This should be documented.

Additionally, I see no documentation on which behaviors are push and which are pull.

And the distinction isn't mentioned in the readme at all.

Aurelia has another "behavior" concept

Just wanted to mention, the Aurelia framework talks about its own behavior concept which might get confused with one used here:

http://aurelia.io/hub.html#/doc/article/aurelia/templating/latest/templating-html-behaviors-introduction/2

http://stackoverflow.com/questions/43012682/aurelia-how-to-add-binding-behavior-in-a-custom-element-and-within-its-own-names

aurelia/templating-resources#137

https://www.tutorialspoint.com/aurelia/aurelia_binding_behavior.htm

You can think of binding behavior as a filter that can change binding data and display it in different format.

Just wanted to let you know.

It is probably too general term for something specific, so more future confusion is likely guaranteed :) There can be just too many things that people will like to call that way.

Perhaps something like "continuous eventless stream" would better describe the concept, even if too long. Or perhaps some abbreviation like CES? :) At least no one will try to use if for other things.

Derive methods from general accumulators?

It seems that many methods can be derived from the following general accumulator combinator:

accum<A>(init: Behavior<A>, fnStream: Stream<Behavior<(a: A) => A>>): Behavior<Behavior<A>>

Here accum(init, fnStream) begins with init and is mapped successively through each function in the fnStream:

const accum = (init, fnStream ) => t1 => t2 =>
    fnStream
        .filter({ time } => (time >= t1) && (time <= t2))
        .map({ value } => value(t))
        .reduce((acc, feed)=>feed(acc), init(t))

Now we can derive:

const stepper = (init, stream) =>
    accum(Behavior.of(init), stream.map(a => Behavior.of(b => a)))

const switcher = (init, streamBeh) =>
    accum(init, streamBeh.map(beh => t => a => beh(t)))

const scan = (fn, init, stream) => 
    accum(Behavior.of(init), stream.map(a => Behavior.of(b => fn(a, b)))

Is this correct?

Changes :: behavior -> stream?

https://github.com/funkia/hareactive#changesab-behaviora-streama

I am a bit confused about this one, e.g. if the behaviour is modelled by the mouse move, how is it translated into the continuous stream?

It is used in the example

https://github.com/funkia/turbine/blob/master/examples/zip-codes/index.ts#L41

in a way that feels a bit like throwing away the available information coming from the typing
events from the inputValue, which might be easier to model as (discrete) stream for precisely this reason. As stream it would emit the new value at the right time and you could save a line and a method, it would seem.

I might be missing something of course.

API documentation incomplete

I can't find any docs for moment And, the difference from lift isn't clear. Over here you explain it kinda,
funkia/turbine#51 (comment) but that should be more promiment

I can't find any docs for accumCombine. I think I see what it does, though.

I'm sure there's more but that's just what I noticed while looking through TodoMVC.

Are these docs autogenerated? I could try to write some docs but I want to make sure I understand if they're gen'd.

Add scanCombine

scanCombine([
  [prepend, (item, list) => [item].concat(list)],
  [removeKey, (keys, list) => list.filter(item => !contains(itemToKey(item), keys))],
], initial);

Future questions

Firstly, I think the 0.0.9 javascript node_modules/future.js file in the npm release is not in sync with the src/future.ts. I can't find sinkFuture to import and it is not in the JavaScript file.

I was trying to figure out if it was possible to replace Task from folktale's data.task with the hareactive Future :).

these parts:

function read(path) {
  return new Task(function(reject, resolve) {
    fs.readFile(path, function(error, data) {
      if (error)  reject(error)
      else        resolve(data)
    })
  })
}

...

concatenated.fork(
  function(error) { throw error }
, function(data)  { console.log(data) }
)

Initialization of behaviors and events

I've been thinking about how best to initialize behaviors and events.

Goals:

  • Initialization should only be allowed to happen once
  • It should be easy to find where in the view a thing is being initialized

This is what we currently have. Initialization can be found by searching for "name.def".

{click: btnClick.def} = button("Click me")

This one looks very non-magical. One would have to find initialization locations by searching for ": name".

button("Click me", {on: {click: btnClick})

This one also has no magic. The def function (or whatever it should be called) extracts events from the component.

def({click: btnClick}, button("Click me")

In this one the events list specifies which events to create. But it doesn't work, because the result of the expression whould not be the component but instead the array.

[btnClick.def] = button("Click me", {events: ["click"]})

`changes` should work on pulling behavior

Hard to implement but would be nice.

Use case:

const isAfterEnd = timeB.map((t) => t >= duration);
const afterEnd = filter((b) => b, changes(isAfterEnd));

However, I think that implementing this would require Stream to also have a push/pull mode just like behaviors. I.e. the afterEnd stream above would be in pull-mode.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.