This is the core module of Elerea, which contains the signal implementation and the primitive constructors.
The basic idea is to create a dataflow network whose structure closely
resembles the user's definitions by turning each combinator into a
mutable variable (an
IORef). In other words, each signal is
represented by a variable. Such a variable contains information about
the operation to perform and (depending on the operation) references
to other signals. For instance, a pointwise function application
created by the
<*> operator contains an
SNA node, which holds two
references: one to the function signal and another to the argument
In order to have a pure(-looking) applicative interface, the library
unsafePerformIO to create the references on demand. In
contrast, the execution of the network is explicitly marked as an IO
operation. The core library exposes a single function to animate the
superstep, which takes a signal and a time interval,
and mutates all the variables the signal depends on. It is supposed
to be called repeatedly in a loop that also takes care of user input.
To ensure consistency, a superstep has two phases: evaluation and
finalisation. During evaluation, each signal affected is sampled at
the current point of time (
sample), advanced by the desired time
advance), and both of these pieces of data are stored in its
reference. If the value of a signal is requested multiple times, the
sample is simply reused, and no further aging is performed. After
successfully sampling the top-level signal, the finalisation process
throws away the intermediate samples and marks the aged signals as the
current ones, ready to be sampled again. Evaluation is done by the
signalValue function, while finalisation is done by
these functions are invoked recursively on a data structure with
existential types, their types also need to be explicity quantified.
As a bonus, applicative nodes are automatically collapsed into lifted functions of up to five arguments. This optimisation significantly reduces the number of nodes in the network.
- type Time = Double
- type DTime = Double
- type Sink a = a -> IO ()
- newtype Signal a = S (IORef (SignalTrans a))
- data SignalTrans a
- data SignalNode a
- = SNK a
- | SNF (Time -> a)
- | SNS a (DTime -> a -> a)
- | forall t . SNT (Signal t) a (DTime -> t -> a -> a)
- | forall t . SNA (Signal (t -> a)) (Signal t)
- | SNE (Signal a) (Signal Bool) (Signal (Signal a))
- | SNR (IORef a)
- | forall t . SNL1 (t -> a) (Signal t)
- | forall t1 t2 . SNL2 (t1 -> t2 -> a) (Signal t1) (Signal t2)
- | forall t1 t2 t3 . SNL3 (t1 -> t2 -> t3 -> a) (Signal t1) (Signal t2) (Signal t3)
- | forall t1 t2 t3 t4 . SNL4 (t1 -> t2 -> t3 -> t4 -> a) (Signal t1) (Signal t2) (Signal t3) (Signal t4)
- | forall t1 t2 t3 t4 t5 . SNL5 (t1 -> t2 -> t3 -> t4 -> t5 -> a) (Signal t1) (Signal t2) (Signal t3) (Signal t4) (Signal t5)
- debugLog :: String -> IO a -> IO a
- createSignal :: SignalNode a -> Signal a
- signalValue :: forall a. Signal a -> DTime -> IO a
- commit :: forall a. Signal a -> IO ()
- advance :: SignalNode a -> DTime -> IO (SignalNode a)
- sample :: SignalNode a -> DTime -> IO a
- timeRef :: IORef Time
- superstep :: Signal a -> DTime -> IO a
- time :: Signal Time
- stateless :: (Time -> a) -> Signal a
- stateful :: a -> (DTime -> a -> a) -> Signal a
- transfer :: a -> (DTime -> t -> a -> a) -> Signal t -> Signal a
- latcher :: Signal a -> Signal Bool -> Signal (Signal a) -> Signal a
- external :: a -> IO (Signal a, Sink a)
Some type synonyms
The data structures behind signals
A signal is represented as a transactional structural node.
|Eq (Signal a)|
The equality test checks whether to signals are physically the same.
|Fractional t => Fractional (Signal t)|
|Num t => Num (Signal t)|
|Show (Signal a)|
A node can have two states: stable (freshly created or finalised) or mutating (in the process of aging).
The possible structures of a node are defined by the
type. Note that the
SNLx nodes are only needed to optimise
applicatives, they can all be expressed in terms of
|SNF (Time -> a)|
|SNS a (DTime -> a -> a)|
|forall t . SNT (Signal t) a (DTime -> t -> a -> a)|
|forall t . SNA (Signal (t -> a)) (Signal t)|
|SNE (Signal a) (Signal Bool) (Signal (Signal a))|
|SNR (IORef a)|
|forall t . SNL1 (t -> a) (Signal t)|
|forall t1 t2 . SNL2 (t1 -> t2 -> a) (Signal t1) (Signal t2)|
|forall t1 t2 t3 . SNL3 (t1 -> t2 -> t3 -> a) (Signal t1) (Signal t2) (Signal t3)|
|forall t1 t2 t3 t4 . SNL4 (t1 -> t2 -> t3 -> t4 -> a) (Signal t1) (Signal t2) (Signal t3) (Signal t4)|
|forall t1 t2 t3 t4 t5 . SNL5 (t1 -> t2 -> t3 -> t4 -> t5 -> a) (Signal t1) (Signal t2) (Signal t3) (Signal t4) (Signal t5)|
You can uncomment the verbose version of this function to see the applicative optimisations in action.
Internal functions to run the network
This function is really just a shorthand to create a reference to a given node.
Sampling and aging the signal and all of its dependencies, at the same time. We don't need the aged signal in the current superstep, only the current value, so we sample before propagating the changes, which might require the fresh sample because of recursive definitions.
Aging the signal. Stateful signals have their state forced to prevent building up big thunks, and the latcher also does its job here. The other nodes are structurally static.
Sampling the signal at the current moment. This is where static
nodes propagate changes to those they depend on. Note the latcher
SNE): the signal is sampled before latching takes place,
therefore even if the change is instantaneous, its effect cannot be
observed at the moment of latching. This is needed to prevent
dependency loops and make recursive definitions involving latching
possible. The stateful signals
SNT are similar, although
it is only the transfer function where it matters that the input
signal cannot affect the current output, only the next one.
|:: Signal a|
the top-level signal
the amount of time to advance
|-> IO a|
the value of the signal before the update
Advancing the whole network that the given signal depends on by
the amount of time given in the second argument. Note that the shared
time signal is also advanced, so this function should only be used
for sampling the top level.
A pure stateful signal.
|-> (DTime -> t -> a -> a)|
state updater function
|-> Signal t|
|-> Signal a|
A stateful transfer function. The current input can only affect the next output, i.e. there is an implicit delay.
|:: Signal a|
|-> Signal Bool|
|-> Signal (Signal a)|
|-> Signal a|
Reactive signal that starts out as
s and can change its
behaviour to the one supplied in
e is true. The change
can only be observed in the next instant.