Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/rogual/frp.js

Functional Reactive Programming primitives for JavaScript
https://github.com/rogual/frp.js

Last synced: 2 months ago
JSON representation

Functional Reactive Programming primitives for JavaScript

Awesome Lists containing this project

README

        

# frp.js 0.3

This is a library providing functional reactive programming (FRP) primitives and
operations.

# Installation

npm install frp

# Conventions

Where appropriate, functions are provided both as module-level functions and as
methods on the FRP objects themselves.

Wherever it makes sense, functions accepting callbacks will also accept
lodash-style callback specifiers instead of functions.

# Concepts

## Event

require('frp/event')
require('frp').event

An event is something that can be watched for a value:

event.watch(cb)
event.bind(cb)

event.unwatch(cb)
event.unbind(cb)

The two forms are equivalent.

The given callback may be called any number of times in the future, with one
argument (the “value”) each time. This is referred to as the event “firing” that
value.

The `watch` and `bind` functions return a function which can be used instead of
the `unwatch` or `unbind` functions.

### Constructing Events

#### `frp.event.pipe()`

This returns a Pipe, which is an event that has an extra function `fire(value)`
which causes it to fire a value. It also has a property `event` which refers to
a copy of itself without the fire function.

A useful pattern is to create a pipe, keep a reference to it, and return
pipe.event to your caller so that other code can listen to the event, but not
cause it to fire.

### Combining Events

#### `frp.event.combine(eventsArray)`

Returns a new event which, whenever any of the given events fires a value, fires
that value.

### Constant Events

#### `frp.event.identity`

This is an event which never fires and as such is the identity with respect to
`Event.combine`.

### Functions on Event


map(event, fn)
event.map(fn)

Returns a new event which, when the original event fires a value, fires
`fn(value)`.


multimap(event, fn)
event.multimap(fn)

Returns a new event which, when the original event fires a value, calls `fn(value, emit)`, and fires any values passed to `emit`.

The given `fn` may call `emit` 0, 1, or n times. It may also store `emit` and arrange for it to be called after `fn` returns.

It is guaranteed that, in all calls to `fn`, `emit` will always refer to the same function object.


filter(event, fn)
event.filter(fn)

Returns a new event which, when the original event fires a value, fires
`value` if `fn(value)` is truthy. If not given, `fn` defaults to the identity
function.


exclude(event, fn)
event.exclude(fn)

Returns a new event which, when the original event fires a value, fires
`value` if `fn(value)` is falsy. If not given, `fn` defaults to the identity
function.


unique(event)
event.unique()

unique(event, equals)
event.unique(equals)

Returns a new event which fires only the unique values fired by the given
event. A unique value is any value not equal to the previous value fired.

If you pass in `equals`, it should be a two-argument function returning
`true` if the two values should be considered equal.

If you don't pass in `equals`, JavaScript's object identity operator (`===`)
will be used.


fold(event, initial, fn)
event.fold(initial, fn)

Returns a new event which keeps an internal value, initialized to the given
initial value. When the given event fires a value `x`, the returned event will
update its internal value to `fn(oldInternal, x)` and fire that value.


reduce(event, fn)
event.reduce(fn)

Waits for the event to fire a value `x`, then behaves as `event.fold(x, fn)`.


ref(event)
event.ref()

Returns a new event which behaves identically to the given event, but which also
has a `release` function. When called, this breaks the link between the original
event and the returned event.


sum(event)
event.sum()

Equivalent to `event.fold(0, add)`


product(event)
event.product()

Equivalent to `event.fold(1, multiply)`


sync(eventA, eventB)
eventA.sync(eventB)

Returns a new event which, whenever `eventA` fires, adds the fired value to
a queue. When `eventB` fires, the returned event fires all values in the
queue in succession, and the queue is flushed.

## Signal

require('frp/signal')
require('frp').signal

A signal represents a changing value. It is like an event except that it has
a current value:

signal.value
signal.get()

Signals can also be empty, in which case `signal.empty` will be true and
`signal.value` and `signal.get()` will be undefined.

Signals can be watched, just like events:

signal.watch(cb)
signal.unwatch(cb)

There is also a similar function, `bind`:

signal.bind(cb)
signal.unbind(cb)

In this case, `cb(value)` will be called whenever the signal’s value changes
just like with `watch().` Additionally, though, `cb(value)` will be called once
as soon as `bind()` is called, with the signal’s current value.

### Constructing Signals

#### `frp.signal.constant(value)`

Returns a signal whose value is always the same.

#### `frp.signal.event(event)`

Returns an empty signal whose value is updated with any value fired by the given
event.

#### `frp.signal.event(initial, event)`

Returns a signal with the given initial value, but whose value is updated with
any value fired by the given event.

#### `frp.signal.cell()`

Returns a `Cell` object, which is to a signal what a pipe is to an event; that
is, an object like a signal but which can be written to.

Write to a cell using either its `set()` method or its `value` property.

Use `cell.signal` to get an ordinary read-only signal from the cell.

### Combining Signals

#### `frp.signal.combine(signals)`

Accepts either an array of signals or an object mapping names to signals.

If given an object, returns a signal whose value is an object mapping names to the values of the
named signals in the argument object. If any of the named signals are empty, their names will
not be present in the returned signal’s value.

If given an array, returns a signal whose value is an array whose elements are
the values of the corresponding signals in the given array. If any of the given
signals are empty, the corresponding items in the value array will be left unset.

#### `frp.signal.join(signals)`

Accepts either an array of signals or an object mapping names to signals.

Behaves as `Signal.combine`, except `join` waits for all given signals to have
values before starting to emit values.

### Functions on Signal

Signals support all the functions that events do. See the unit tests for details
on how these differ from their counterparts on event.

Signals also offer the following functions:


initial(signal, value)
signal.initial(value)

If the given signal has a value, returns the given signal. Otherwise, returns
a signal which has the given value, and which starts to track the given signal’s
value as soon as it has one.


flatten(signal)
signal.flatten()

The argument should be a signal whose value is always another signal.

Returns a new signal whose value is always the value of the signal that is the
value of the given signal.

If the given signal ever takes on a value which is not a signal, undefined
behaviour ensues.


flatMap(signal, fn)
signal.flatMap(fn)

Equivalent to `signal.map(fn).flatten()`

# Lifetime Management

When using transformative functions like `map` or `filter` to create new events
and signals out of old ones, the old (“parent”) event or signal will retain
a reference to the new (“child”) signal for as long as it lives, unless it is
specifically unbound.

This means that **child objects will live at least as long as their parents!**

So, if you have a long-lived object and you keep calling `map` on it, its
internal list of watchers will keep growing ever longer, even after you’ve
stopped using the new objects that `map` has created.

This may not be what you want, which is where the `ref` function comes in:


signal.ref()
event.ref()
ref(signal)
ref(event)

Calling `ref` on a signal or event creates an object that behaves identically
to the object you called `ref` on, except that the new object will have a
`release` method. Calling this will cause it to become disconnected from its
parent, no longer mirroring its updates.

This allows you to manage the lifetime of connected chains of events and
signals. For example:

var a = getSuperLongLivedEvent();

var a0 = a.ref();
a0.map(blah).filter(bling).watch(function(x) {
...
});

// done with a0
a0.release();

var a1 = a.ref();
a1.map(bloo).filter(blarp).watch(function(y) {
...
});

// done with a1
a1.release();

In this example, the intermediary events created by the calls to `map` and
`filter` would normally stick around forever, bloating `a`’s list of watchers.
But, since `a0` and `a1` are created using `a.ref`, calling `release` causes
them to disconnect from `a` and become unreachable. `a`’s list of watchers will
be empty again after both `a0` and `a1` are released, and `a0` and `a1` can
be garbage-collected.

# Error Handling

This library does not take a stance on error handling. Events and Signals
concern themselves only with values, and the question of which values are
considered ‘errors’ is left up to you and your application.

Exceptions are not afforded any particular special handling; exceptions
thrown by your code are not caught by `frp.js` and so will propagate up
the stack as normal.

In general, this means that if an exception is thrown in the transformation
function given to `map` or `filter` or similar, this will propagate up
to where you called `set` or `fire`, and the transformed event or signal
(the return value of `map` or similar) will not fire a value.