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)
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)
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)
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)
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.
fold(event, initial, fn)
event.fold(initial, fn)
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)
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()
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()
sum(event) event.sum()
Equivalent to event.fold(0, add)
product(event)
event.product()
product(event) event.product()
Equivalent to event.fold(1, multiply)
sync(eventA, eventB)
eventA.sync(eventB)
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)
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()
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)
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)
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.