ReactiveBasics
This package implements basic functionality for a type of Functional Reactive Programming (FRP). This is a style of DataFlow programming where updates propagate to other objects automatically. It follows the same basic API as Reactive.jl. Much of the documentation for Reactive applies to ReactiveBasics.
Example
The Signal
type holds values that can depend on other Signal
s. map(f, xs...)
returns a new Signal that depends on one or more Signals xs...
. The function f
defines the value that the Signal should take as a function of the values of each of the input Signals. When a Signal
is updated using push!
, changes propagate to dependent Signal
s. Here is an example taken from Reactive.jl:
julia> using ReactiveBasics
julia> x = Signal(0)
ReactiveBasics.Signal{Int64}(0, Function[])
julia> value(x)
0
julia> push!(x, 42)
julia> value(x)
42
julia> xsquared = map(a -> a*a, x)
ReactiveBasics.Signal{Int64}(1764, Function[])
julia> value(xsquared)
1764
julia> push!(x, 3)
julia> value(xsquared)
9
Various utility functions are available to manipulate signals, including:
subscribe!
– Subscribe to the changes of a Signal.merge
– Combine Signals.zip
– Combine Signals as a Tuple.zipmap
– Zip then map.filter
– A Signal filtered based on a function.filterwhen
– A Signal filtered based on a Signal.foldp
– Fold/map over past values.flatmap
– Likemap
, but it's meant for functions that returnSignal
s.asyncmap
– Likemap
, but it updates asynchronously.flatten
– Flatten a Signal of Signals.bind!
– Bind two Signals, so that updates to one are synchronized with the other.droprepeats
– Drop repeats in the input Signal.previous
– A Signal with the previous value of the input Signal.sampleon
– Sample one Signal when another changes.preserve
– No-op for compatibility with Reactive.
Change propagation
The main difference between ReactiveBasics and Reactive.jl is that Signals propagate immediately (synchronous operation) in ReactiveBasics. There is no event queue. The implementation uses closures derived from the approach used in the Swift package Interstellar. The difference in operation makes ReactiveBasics as much as ten times faster than Reactive. But, because ReactiveBasics is synchronous, this leads to limitations. One is that there can be race conditions for asynchronous inputs. Those have to be manually handled. Another issue is that calculations can be triggered twice if there are mutual dependencies.
ReactiveBasics uses push-style reactive programming. When push!
is used to update a signal, dependencies of this Signal update in depth-first fashion.
Here is an example that leads to double calculations:
using ReactiveBasics
x = Signal(2)
x2 = map(u -> 2u, x)
y = map(+, x2, x)
subscribe!(u -> println("value of y: $u"), y)
push!(x, 3)
value of y: 9
value of y: 9
This push!
will trigger y
to update twice. The update to x
triggers the update to x2
. The update to x2
triggers the update to y
. But because y
also depends on x
, it updates a second time. Both times, the resulting value for y
is right, but this effect will be important if a Signal accumulates or otherwise depends on history or the number of calculations.
Handling asynchronous Signals
Even though ReactiveBasics uses direct, push-style processing, it is possible to handle asynchronous Signals. For long calculations or for input/output, it's often convenient to return a Signal with the end result rather than just a value. That allows updates to propagate correctly. flatmap
is a useful utility for managing operations that return Signals. See the space-station example by GitHub user nixterrimus.
Another way to handle asynchronous Signals is to set up a queue of Signals. See this example.
Other notes
This is a basic implementation. There is no support for error checking, time, or sampling. My main use case is with Sims, and that doesn't need a lot of features.