Functional Reactive Programming (FRP) deals with with Event Processing, data flow and propagation of change.It is a composable/modular way to code event-driven logic. Events come in while the program is running, most often, at unpredictable times and should be processed and routed to multiple destinations. We can describe these dataflow connections as a network, connecting inputs to outputs. The network itself is functional, i.e., it's formed by chaining functions which do not present any visible state to the user and no not cause side-effects (posting, chainging synths, changing outside variables, etc).
The FRP classes follow more or less the API of reactive-banana (Haskell library - http://www.haskell.org/haskellwiki/Reactive-banana) and were originally based on code from reactive-web (Scala library - http://reactive-web.tk/).
The key points to keep in mind regarding FRP are:
Thw two main abstractions of this FRP library are event streams and signals.
You can think of EventStreams as Collections of elements tagged with a time value. It is encoded with subclasses of EventStream.
Signals difer from EventStreams in that it's value is know at each moment in time. Signals are encoded with the FPSignal class hierarchy. In FPLib signals are always step functions, that is they hold a value constant until the next value arrives.
The main use for signals is to keep around the emmited value. This is handy for:
It's possible to create a signal from an event stream and vice-versa by using the 'changes' and 'hold' methods.
The event network graph can be specified using EventNetwork or the jit style ENdef. The recommended way to use FRP in FPLib is with ENdef as seen in the example below. The do and fire methods should be used only for quick testing, not for actual instruments or guis.
Complex networks for event processing are created using the combinator methods.
You can think of EventStreams as Collections of elements over time. Therefore the methods of collections will work as you expect.
eventSource.select(f)
only outputs a value if f.(value)
is true.
x.select(_<0.04)
eventSource.collect(f)
returns a new EventStream, that for every event that this EventStream fires, that one will fire an event that is the result of applying 'f' to this EventStream's event.
eventSource.inject(initialState,f)
This method can be used to keep state or accumulate values. f must receive two variables, the first is the current state and second is the last value received. Only works on event streams as signals must always have a current value.
Sum values:
Add values to array:
Keep last two values:
This method allows selecting which events to output depending on the values output by some other EventStream. This is a form of dynamic graph change, so based on a new event a node might be connected a different node, or even a new node created. Many common situations can be dealt with without using switchTo.
es.switchTo(f)
creates a new EventStream that consists of the events of the EventStreams returned by f. f is applied on every event of the original EventStream, and its returned EventStream is used until the next event fired by the original EventStream, at which time the previously returned EventStream is no longer used and a new one is used instead.
A common requirement is to combine the output of n signals into 1 signal by running an n-argument function on each incoming value of the signals. This is done using the applicative functor combinators (<*>, <%> ):
In a language such as Haskell it is possible to have mutually recursive values. These are values where they both appear in the definition of each other. This would let us define an event stream that depends on it's own past values by defining it it terms of the signal obtained from it with hold. The code for an event stream that sums incoming values with its last value looks like:
Unfortunatelly this cannot be done in SuperCollider since we cannot define mutually recursive variables. To get around this limitation we can directly probe the value of the signal obtained from hold using the 'now' method. The now method is essentially performing an IO operation which usually wouldn't be acceptable inside an event graph, but for this particular case it is very handy and doesn't have drawbacks. The code above then translates in FPLib to:
Because the call to now is done inside an anonymous function is delayed and therefore doesn't cause an error at the definition line.
Usually it is recommended to use EventNetwork for automatic management of connections. In any case it's also possible manage connections explicitelly.
Suppose we have the following network:
We can reset the original EventSource which will remove all the branches that start from it. Those branches will be garbage collected.
If you want to disconnect only some branches you can use remove :
We can also disconnect part of the network conditionally on the incoming values: