Personal tools

Netwire

From HaskellWiki

(Difference between revisions)
Jump to: navigation, search
(Signal analysis)
m (Predefined wires)
(36 intermediate revisions by 4 users not shown)
Line 1: Line 1:
Netwire is a library for [[Functional Reactive Programming|functional reactive programming]], which uses the concept of [[Arrow|arrows]] for modelling an embedded domain-specific language. This language lets you express reactive systems, which means systems that change over time. It shares the basic concept with [[Yampa]] and its fork Animas, but it is itself not a fork.
+
Netwire is a library for [[Functional Reactive Programming|functional reactive programming]], which uses the concept of [[Arrow|arrows]] for modelling an embedded domain-specific language. This language lets you express reactive systems, which means systems that change over time. It shares the basic concept with [[Yampa]] and its fork Animas, but it is itself not a fork and has many additional features.
   
:[http://hackage.haskell.org/package/netwire Download netwire]
+
* [http://hackage.haskell.org/package/netwire Download netwire]
  +
  +
This wiki page corresponds to Netwire version 3 and is currently a work in progress.
   
   
 
== Features ==
 
== Features ==
   
Here is a list of some of the features of ''netwire'':
+
Here is a list of some of the features of Netwire:
   
* arrowized interface,
+
* arrow interface (or optionally an applicative interface),
* applicative interface,
+
* signal inhibition (ArrowZero / Alternative),
* signal inhibition (''ArrowZero'' / ''Alternative''),
+
* signal selection (ArrowPlus / Alternative),
* choice and combination (''ArrowPlus'' / ''Alternative''),
+
* self-adjusting wires (ArrowChoice),
* self-adjusting wires (''ArrowChoice''),
 
 
* rich set of event wires,
 
* rich set of event wires,
 
* signal analysis wires (average, peak, etc.),
 
* signal analysis wires (average, peak, etc.),
* impure wires.
+
* effectful wires.
   
== Quickstart ==
 
   
This is a quickstart introduction to Netwire for Haskell programmers familiar with arrowized functional reactive programming (AFRP), for example Yampa or Animas. It should quickly give you an idea of how the library works and how it differs from the two mentioned.
+
== Basics ==
   
=== The wire ===
+
The Netwire library is based around a data type called <hask>Wire</hask>. You need to import the <hask>Control.Wire</hask> module to work with wires:
 
Netwire calls its signal transformation functions ''wires''. You can think of a wire as a device with an input line and an output line. The difference between a function and a wire is that a wire can change itself throughout its lifetime. This is the basic idea of arrowized FRP. It gives you time-dependent values.
 
 
A wire is parameterized over its input and output types:
 
   
 
<haskell>
 
<haskell>
data Wire a b
+
import Control.Wire
</haskell>
 
   
+
data Wire e (>~) a b
=== Differences from Yampa ===
 
 
If you are not familiar with Yampa or Animas, you can safely skip this section.
 
 
The main difference between Yampa and Netwire is that the underlying arrow is impure. While you can choose not to use the impure wires inside of the '''FRP.NetWire.IO''' module, it is a design choice for this library to explicitly allow impure computations. One theoretical implication is that you need to differentiate between pure stateless, pure stateful and impure signal transformations.
 
 
A concept not found in Yampa is signal inhibition. A wire can choose not to return anything. This way you can temporarily block entire subnetworks. This is most useful with the combination operator ''<+>''. Example:
 
 
<haskell>
 
w = w1 <+> w2
 
 
</haskell>
 
</haskell>
   
The ''w'' wire runs its signal through the wire ''w1'', and if it inhibits, it passes the signal to ''w2''.
+
For some arrows <hask>(>~)</hask> and all monoids <hask>e</hask> the type <hask>Wire e (>~)</hask> is an arrow. Only certain arrows are allowed for <hask>(>~)</hask>, because <hask>Wire</hask> is actually a data family. These arrows are called base arrows in Netwire.
 
Another concept not found in Yampa is choice. Through the ''ArrowChoice'' instance wires allow you to choose one of a set of subwires for its signal without needing a switch. Essentially you can write ''if'' and ''case'' constructs inside of arrow notation.
 
 
Because of their impurity wires do not have an ''ArrowLoop'' instance. It is possible to write one, but it will diverge most of the time, rendering it useless.
 
 
 
=== Using a wire ===
 
 
To run a wire you will need to use the ''withWire'' and ''stepWire'' functions. The ''withWire'' initializes a wire and gives you a ''Session'' value. As metioned earlier in general a wire is a function, which can mutate itself over time. The session value captures the current state of the wire.
 
   
 
<haskell>
 
<haskell>
initWire :: Wire a b -> (Session a b -> IO c) -> IO c
+
comp :: Wire e (>~) a b
stepWire :: a -> Session a b -> IO (Maybe b)
 
 
</haskell>
 
</haskell>
   
The ''stepWire'' function passes the given input value through the wire. If you use ''stepWire'', then the wire will mutate in real time. If you need a different rate of time, you can use ''stepWireDelta'' or ''stepWireTime'' instead. The ''stepWireDelta'' function takes a time delta, and the ''stepWireTime'' function takes the current time (which doesn't need to be the real time):
+
Values of type <hask>Wire e (>~) a b</hask> are time-varying functions, which resemble the following type:
   
 
<haskell>
 
<haskell>
stepWireDelta :: Double -> a -> Session a b -> IO (Maybe b)
+
a >~ Either e b
stepWireTime :: UTCTime -> a -> Session a b -> IO (Maybe b)
 
 
</haskell>
 
</haskell>
   
Note that it is allowed to give zero or negative deltas and times, which are earlier than the last time. This lets you run the system backwards in time. If you do that, your wire should be prepared to handle it properly.
+
So it's a function that takes a value of type <hask>a</hask> and either produces a value of type <hask>b</hask> or produces no value, but instead ''inhibits'' with a value of type <hask>e</hask>. The act of running a wire is called ''stepping'' and the process is called an ''instant''. You can step a wire through one of the stepping functions, which we will cover later. When you step a wire, it will return a new version of itself along with its result. You are supposed to call the new version the next time you step.
   
The stepping functions return a ''Maybe b''. If the wire inhibits, then the result is ''Nothing'', otherwise it will be ''Just'' the output. Here is a complete example:
+
=== The inhibition monoid ===
   
<haskell>
+
The <hask>e</hask> argument to <hask>Wire</hask> is called the inhibition monoid. For simple applications you can just use <hask>()</hask> here, but you may want to actually assign exception values to inhibition. We will cover that later. For now just use <hask>()</hask>.
{-# LANGUAGE Arrows #-}
 
   
module Main where
+
=== Base arrows ===
   
import Control.Monad
+
The <hask>(>~)</hask> argument to <hask>Wire</hask> is called the base arrow. In most cases you will use a <hask>Kleisli</hask> arrow here, and this is currently the only type of arrow supported, though more will be added in the future. For simple applications you can just use the <hask>IO</hask> monad, and it is useful to define a type alias for your custom wire type:
import FRP.NetWire
 
import Text.Printf
 
   
+
<haskell>
myWire :: Wire () String
+
type MyWire = Wire () (Kleisli IO)
myWire =
 
proc _ -> do
 
t <- time -< ()
 
fps <- avgFps 1000 -< ()
 
fpsPeak <- highPeak -< fps
 
 
if t < 4
 
then identity -< "Waiting four seconds."
 
else identity -<
 
printf "Got them! (%8.0f FPS, peak: %8.0f)"
 
fps fpsPeak
 
 
 
main :: IO ()
 
main = withWire myWire loop
 
where
 
loop :: Session () String -> IO ()
 
loop session =
 
forever $ do
 
mResult <- stepWire () session
 
case mResult of
 
Nothing -> putStr "Signal inhibted."
 
Just x -> putStr x
 
putChar '\r'
 
 
</haskell>
 
</haskell>
   
This program should display the string "Waiting four seconds." for four seconds and then switch to a string, which displays the current average frames per second and peak frames per second.
 
   
Note: Sessions are thread-safe. You are allowed to use the stepping functions for the same session from multiple threads. This makes it easy to implement conditional stepping based on system events.
+
== Running wires ==
   
== Writing a wire ==
+
For running a wire you can use the stepping functions available in the <hask>Control.Wire.Session</hask> module. There is no need to import that module. It is automatically imported with <hask>Control.Wire</hask>. For Kleisli-based wires you will want to use the <hask>stepWireM</hask> function:
 
I will assume that you are familiar with arrow notation, and I will use it instead of the raw arrow combinators most of the time. If you haven't used arrow notation before, see the [http://www.haskell.org/ghc/docs/latest/html/users_guide/arrow-notation.html GHC arrow notation manual].
 
 
=== Time ===
 
 
To use this library you need to understand the concept of time very well. Netwire has a continuous time model, which means that when you write your applications you disregard the discrete steps, in which your wire is executed.
 
 
Technically at each execution instant (i.e. each time you run ''stepWire'' or one of the other stepping functions) the wire is fed with the input as well as a time delta, which is the time passed since the last instant. Hence wires do not by themselves keep track of what time it is, since most applications don't need that anyway. If you need a clock, you can use the predefined ''time'' wire, which will be explained later.
 
 
Wires have a local time, which can be different from the global time. This can happen, when a wire is not actually run, because an earlier wire inhibited the signal. It also happens, when you use choice. For example you can easily write a gateway, which repeatedly runs one wire the one second and another wire the other second. While one wire is run, the other wire is suspended, including its local time.
 
 
Local time is a switching effect, which is especially visible, when you use the switching combinators from '''FRP.NetWire.Switch'''. Local time starts when switching in.
 
 
Time is measured in ''Double'' in Netwire. To improve type signatures there are two type aliases defined for you:
 
   
 
<haskell>
 
<haskell>
type DTime = Double
+
stepWireM ::
type Time = Double
+
Monad m
  +
=> Wire e (Kleisli m) a b
  +
-> a
  +
-> m (Either e b, Wire e (Kleisli m) a b)
 
</haskell>
 
</haskell>
   
While ''Time'' refers to time, ''DTime'' refers to time deltas, i.e. time differences.
+
In our case we have <hask>m = IO</hask>, so our type signature is simply:
 
=== Pure stateless wires ===
 
 
Pure stateless wires are easy to explain, so let's start with them. A pure stateless wire is essentially just a function of input. The simplest wire is the ''identity'' wire. It just returns its input verbatim:
 
   
 
<haskell>
 
<haskell>
identity :: Wire a a
+
stepWireM :: MyWire a b -> a -> IO (Either () b, MyWire a b)
 
</haskell>
 
</haskell>
   
If you run such a wire (see the previous section), then you will just get your input back all the time. Another simple wire is the ''constant'' wire, which also disregards time:
+
This function takes a wire and an input value. It passes the input value to the wire and returns its result value of type <hask>Either () b</hask>. Along with the result it also returns a new wire. Normally you would call <hask>stepWireM</hask> in a loop, which performs instant after instant. This is the basic structure:
   
 
<haskell>
 
<haskell>
constant :: b -> Wire a b
+
system :: MyWire Int String
</haskell>
+
system = {- ... -}
   
If you run the wire <code>constant 15</code>, you will get as output the number 15 all the time, regardless of the current time and the input.
+
main :: IO ()
  +
main = loop system
  +
where
  +
loop :: MyWire Int String -> IO ()
  +
loop w' = do
  +
(mx, w) <- stepWireM w' 15
   
:'''Note''': You can express ''identity'' as ''arr id'', but you should prefer ''identity'', because it's faster. Likewise you can express ''constant x'' as ''arr (const x)'', but again you should prefer ''constant''.
+
{- ... do something with mx ... -}
   
=== Pure stateful wires ===
+
loop w -- loop with the new wire.
 
Let's see a slightly more interesting wire. The ''time'' wire will return the current local time. What ''local'' means in this context was explained earlier.
 
 
<haskell>
 
time :: Wire a Double
 
</haskell>
 
 
As the type suggests, time is measured in seconds and represented as a ''Double''. The local time starts from 0 at the point, where the wire starts to run. There is also a wire, which counts time from a different origin:
 
 
<haskell>
 
timeFrom :: Double -> Wire a Double
 
 
</haskell>
 
</haskell>
   
The difference between these stateful and the stateless wires from the previous section is that stateful wires mutate themselves over time. The ''timeFrom x'' wire calculates the current time as ''x'' plus the current time delta. Let's say that sum is ''y''. It then mutates into the wire ''timeFrom y''. As you can see there is no internal clock. It is really this self-mutation, which gives you a clock.
+
Note: Even though the FRP idea suggests it, there is no reason to run wires continuously or even regularly. You can totally have an instant depending on user input, a GUI event or network traffic, so instants can be minutes apart.
   
=== Calculus ===
+
=== Testing wires ===
   
One of the compelling features of FRP is integration and differentiation over time. It is a very cheap operation to integrate over time. In fact the ''time'' wire you have seen in the last section is really just the integral of the constant 1. Here is the type of the ''integral'' wire, which integrates over time:
+
There is a convenient function for testing wires, which does all the plumbing for you. It's called <hask>testWireM</hask>:
   
 
<haskell>
 
<haskell>
integral :: Double -> Wire Double Double
+
testWireM ::
  +
(Show e, MonadIO m)
  +
=> Int
  +
-> m a
  +
-> Wire e (Kleisli m) a String
  +
-> m ()
 
</haskell>
 
</haskell>
   
The argument is the integration constant or starting value. The input is the subject of integration. Let's write a clock, which runs at half the speed of the real clock:
+
For wires returning a string, you can easily test them using this function. The first argument is a FPS/accuracy tradeoff. If it's 100, it will only print the output of every 100th instant. The second argument is an input generator action. At each instant, this action is run and its result is passed as input to the wire. The wire's output is then printed. <hask>testWireM</hask> prints the output continuously on a single line:
   
 
<haskell>
 
<haskell>
slowClock :: Wire a Double
+
main :: IO ()
slowClock = proc _ -> integral 0 -< 0.5
+
main = testWireM 1000 (return 15) system
 
</haskell>
 
</haskell>
   
Since the integration constant is 0, the time will start at zero. Integration becomes more interesting, as soon as you integrate non-constants:
 
   
<haskell>
+
== Writing wires ==
particle :: Wire a Double
 
particle =
 
proc _ -> do
 
v <- integral 1 -< -0.1
 
integral 15 -< v
 
</haskell>
 
   
This wire models a one-dimensional particle, which starts at position 15 and velocity +1. A constant acceleration of 0.1 per second per second is applied to the velocity, hence the particle moves right towards positive infinity at first, while gradually becoming slower, until it reverses its direction and moves left towards negative infinity.
+
=== Predefined wires ===
   
The above type signature is actually a special case, which i provided for the sake of simplicity. The real type signature is a bit more interesting:
+
There are numerous predefined wires, which you can compose using the arrow interface. We will practice that with three very simple predefined wires (the type signatures are simplified for the sake of learning):
   
 
<haskell>
 
<haskell>
integral ::
+
constant :: b -> Wire e (>~) a b
(NFData v, VectorSpace v, Scalar v ~ Double) =>
+
identity :: Wire e (>~) b b
v -> Wire v v
+
countFrom :: Enum b => b -> Wire e (>~) a b
 
</haskell>
 
</haskell>
   
You can integrate over time in any real vectorspace. Some examples of vectorspaces include tuples, complex numbers and any type, for which you define ''NFData'' and ''VectorSpace'' instances. Let's see the particle example in two dimensions:
+
The ''constant'' function takes an output value and produces a wire which produces that value constantly. So the wire <hask>constant 15</hask> will output 15 constantly at every instant. In other words, <hask>stepWireM</hask> will return <hask>Right 15</hask> along with a new wire that outputs 15 again:
   
 
<haskell>
 
<haskell>
particle2D :: Wire a (Double, Double)
+
stepWireM (constant 15) inp
particle2D =
+
-> (Right 15, constant 15)
proc _ -> do
 
v <- integral (1, -0.5) -< (-0.1, 0.4)
 
integral (0, 0) -< v
 
 
</haskell>
 
</haskell>
   
Differentiation works similarly, although there are two variants:
+
Note the fully polymorphic input type <hask>a</hask>. This basically means that the wire disregards its input, so whatever <hask>inp</hask> is, it is ignored.
 
<haskell>
 
derivative :: Wire Double Double
 
derivativeFrom :: Double -> Wire Double Double
 
</haskell>
 
 
The difference between the two variants is that ''derivative'' will inhibit at the first instant (inhibition is explained later), because it needs at least two samples to compute the rate of change over time. The ''derivativeFrom'' variant does not have that shortcoming, but you need to provide the first sample as an argument.
 
 
Again I have simplified the types to help understanding. Just like with integration you can differentiate over any vectorspace, as long as your type has an ''NFData'' instance.
 
 
=== Events ===
 
 
Events are a useful tool to add discrete values to the system. As the name states an event usually denotes some condition or external event, which can be present at some instants and absent at others. A common use case for events is user input.
 
   
Technically events are nothing special. Since they simply denote values, which can be absent, they are simply ''Maybe'' values. Netwire defines a type alias ''Event'' to enable you to be more specific in your type signatures:
+
The ''identity'' wire is slightly more interesting. It has input and output of type <hask>b</hask>. What it does is: It simply outputs its input value at every instant:
   
 
<haskell>
 
<haskell>
type Event = Maybe
+
stepWireM identity inp
  +
-> (Right inp, identity)
 
</haskell>
 
</haskell>
   
There is a large number of event wires in the '''FRP.NetWire.Event''' module. I will give you examples for some of the common ones here. It is worthwhile to have a look at the aforementioned module.
+
Both identity and constant wires are examples of ''stateless'' wires. They don't change over time. You can see this in the stepping examples above. They always return themselves for the next instant.
   
==== after ====
+
The ''countFrom'' function takes a starting value and returns a wire that returns sequential values instant by instant. This is the first example of a ''stateful'' wire, because it changes over time:
   
 
<haskell>
 
<haskell>
after :: DTime -> Wire a (Event a)
+
stepWireM (countFrom 15) inp
</haskell>
+
-> (Right 15, countFrom 16)
   
The ''after'' wire causes an event after a certain number of seconds. This means that the output signal is ''Nothing'', until the specified time has passed, at which point the output becomes ''Just x'' for a single instant, where ''x'' is the input value at that instant. After that the event never happens again.
+
stepWireM (countFrom 16) inp
+
-> (Right 16, countFrom 17)
==== once ====
 
 
<haskell>
 
once :: Wire (Event a) (Event a)
 
 
</haskell>
 
</haskell>
   
This wire takes a potential event. It waits, until the event happens (i.e. the input becomes a ''Just''). It outputs the event once and then never again, even if the event happens again in the future.
+
=== Composing wires ===
   
==== repeatedly ====
+
The main feature of wires is that you can compose them using the arrow interface. There is a rich set of ways for composing, and you will want to use arrow notation for your convenience:
   
 
<haskell>
 
<haskell>
repeatedly :: Wire (DTime, a) (Event a)
+
system :: MyWire a String
  +
system =
  +
proc _ -> do
  +
c1 <- countFrom 10 -< ()
  +
c2 <- countFrom 20 -< ()
  +
identity -< printf "%d %d" (c1 :: Int) (c2 :: Int)
 
</haskell>
 
</haskell>
   
This wire takes two input signals. It produces events repeatedly after the time delta given by the left signal. This delta can change over time, making the event happen more or less frequently. The right signal is the desired event value.
+
In applications it is common to write wires that ignore their input. For those wires you should make the input type fully polymorphic to indicate this. Running this wire produces:
 
==== hold ====
 
   
 
<haskell>
 
<haskell>
hold :: a -> Wire (Event a) a
+
stepWireM system ()
</haskell>
 
   
This wire turns events into continuous signals. At the beginning the output is the value given by the argument. Each time the input event occurs, the ouput switches to its value and keeps it until the next event occurs.
+
1st instant: Right "10 20"
+
2nd instant: Right "11 21"
=== Random numbers ===
+
3rd instant: Right "12 22"
 
Netwire provides a few wires for random noise generation. Probably the most important one is the ''noise'' wire:
 
 
<haskell>
 
noise :: Wire a Double
 
 
</haskell>
 
</haskell>
   
This wire outputs a random number between 0 (inclusive) and 1 (exclusive). The underlying random number generator is a fast implementation of the Mersenne Twister algorithm provided by Don Stewart's [http://hackage.haskell.org/package/mersenne-random mersenne-random] package.
+
Note: You can use the ''testWireM'' function with this wire. The following action will run the wire continuously printing its result at every 1000th instant:
 
=== Signal analysis ===
 
 
Netwire provides some wires to perform signal analysis. One useful wire is ''diff'':
 
   
 
<haskell>
 
<haskell>
diff :: Eq a => Wire a (Event (a, Time))
+
main :: IO ()
  +
main = testWireM 1000 (return ()) system
 
</haskell>
 
</haskell>
   
This wire emits an event, whenever the input signal changes. The event contains the last value as well as the time elapsed since then. One possible use case is file monitoring. Pass the file's modification time or even its contents as the input signal.
+
In the FRP context we often talk about ''signals''. Particularly in the context of ''arrowized'' FRP (AFRP) like Netwire we talk about ''signal networks'' and signals passing through them. The ''system'' wire is your first signal network. It ignores its input signal and passes the signal <hask>()</hask> to the two counters (which ignore their input signals, too). It takes the output signals <hask>c1</hask> and <hask>c2</hask> and makes a formatted string out of them. Finally this string is passed to the <hask>identity</hask> wire. This is the last wire in the signal network ''system'', so its output signal is the output signal of ''system''. As a side note the ''identity'' wire behaves like ''returnA''.
   
Another useful wire is ''avg'', which computes the average value of the input signal over the specified number of most recent samples:
+
The main feature to note here is that all of the subwires in the composition evolve individually. So in the second instant, each of the two counters will have gone up by one. This alone gives you a powerful abstraction for stateful computations. The equivalent when using a state monad or mutable variables would be to have a global state value with two counter values. By having time-varying functions you can have something called ''local state''. Each of the two counters (or as many as you use) have their own individual local state, which is the current counter value. This is way more convenient and composable than a state monad or other imperative state abstractions.
   
<haskell>
+
=== Choice ===
avg :: Int -> Wire Double Double
 
</haskell>
 
   
Since the ''noise'' wire returns random numbers between 0 and 1, if you pass the output of ''noise'' through ''avg x'' you should get a value close to 0.5, if the argument ''x'' is suitably large:
+
In traditional AFRP solutions like Yampa the path of a signal is fully determined by the structure of the signal network. In Netwire a signal can choose one of multiple paths by using the <hask>case</hask> and <hask>if</hask> constructs:
   
 
<haskell>
 
<haskell>
avgOfNoise :: Wire a Double
+
system =
avgOfNoise = avg 1000 <<< noise
+
proc _ -> do
  +
c1 <- countFrom 10 -< ()
  +
if even c1
  +
then returnA -< "We don't want even c1"
  +
else do
  +
c2 <- countFrom 20 -< ()
  +
returnA -< printf "%d %d" (c1 :: Int) (c2 :: Int)
 
</haskell>
 
</haskell>
   
An interesting special case of ''avg'' is the ''avgFps'' wire, which is very useful for performance analysis. It returns the average frames per second:
+
If the <hask>c1</hask> signal is even, then the wire outputs the string "We don't want even c1". Otherwise it takes the second path. Here it is important to know that the second counter will be suspended, when <hask>c1</hask> is even, because the <hask>else</hask> branch is not reached. A wire can only evolve, when it is actually reached. So in this example <hask>c2</hask> will run at half the speed of <hask>c1</hask> and the output will look like:
 
<haskell>
 
avgFps :: Int -> Wire a Double
 
</haskell>
 
 
Both ''avg'' and ''avgFps'' calculate the average over a certain number of most recent samples. While they have a constant time complexity O(1) they have a linear space complexity of O(n), where ''n'' is the number of samples. In some cases it can be fine to consider calculating the average over all samples forever. The ''avgAll'' wire does exactly that:
 
 
<haskell>
 
avgAll :: Wire Double Double
 
</haskell>
 
 
Unlike ''avg'' and ''avgFps'' this variant uses not only constant time, but also constant space.
 
 
There are also wires for finding peaks. The ''highPeak'' and ''lowPeak'' wires output the high and low peaks respectively for their input:
 
   
 
<haskell>
 
<haskell>
highPeak :: (NFData a, Ord a) => Wire a a
+
1st instant: "We don't want even c1"
lowPeak :: (NFData a, Ord a) => Wire a a
+
2nd instant: "11 20"
  +
3rd instant: "We don't want even c1"
  +
4th instant: "13 21"
  +
5th instant: "We don't want even c1"
  +
6th instant: "15 22"
  +
7th instant: "We don't want even c1"
 
</haskell>
 
</haskell>
   
Again the type signatures are only special cases. See the library documentation for the real types. In short, you can get averages of any fractional input value.
+
[[Category:FRP]]

Revision as of 18:17, 1 December 2011

Netwire is a library for functional reactive programming, which uses the concept of arrows for modelling an embedded domain-specific language. This language lets you express reactive systems, which means systems that change over time. It shares the basic concept with Yampa and its fork Animas, but it is itself not a fork and has many additional features.

This wiki page corresponds to Netwire version 3 and is currently a work in progress.


Contents

1 Features

Here is a list of some of the features of Netwire:

  • arrow interface (or optionally an applicative interface),
  • signal inhibition (ArrowZero / Alternative),
  • signal selection (ArrowPlus / Alternative),
  • self-adjusting wires (ArrowChoice),
  • rich set of event wires,
  • signal analysis wires (average, peak, etc.),
  • effectful wires.


2 Basics

The Netwire library is based around a data type called
Wire
. You need to import the
Control.Wire
module to work with wires:
import Control.Wire
 
data Wire e (>~) a b
For some arrows
(>~)
and all monoids
e
the type
Wire e (>~)
is an arrow. Only certain arrows are allowed for
(>~)
, because
Wire
is actually a data family. These arrows are called base arrows in Netwire.
comp :: Wire e (>~) a b
Values of type
Wire e (>~) a b
are time-varying functions, which resemble the following type:
a >~ Either e b
So it's a function that takes a value of type
a
and either produces a value of type
b
or produces no value, but instead inhibits with a value of type
e
. The act of running a wire is called stepping and the process is called an instant. You can step a wire through one of the stepping functions, which we will cover later. When you step a wire, it will return a new version of itself along with its result. You are supposed to call the new version the next time you step.

2.1 The inhibition monoid

The
e
argument to
Wire
is called the inhibition monoid. For simple applications you can just use
()
here, but you may want to actually assign exception values to inhibition. We will cover that later. For now just use
()
.

2.2 Base arrows

The
(>~)
argument to
Wire
is called the base arrow. In most cases you will use a
Kleisli
arrow here, and this is currently the only type of arrow supported, though more will be added in the future. For simple applications you can just use the
IO
monad, and it is useful to define a type alias for your custom wire type:
type MyWire = Wire () (Kleisli IO)


3 Running wires

For running a wire you can use the stepping functions available in the
Control.Wire.Session
module. There is no need to import that module. It is automatically imported with
Control.Wire
. For Kleisli-based wires you will want to use the
stepWireM
function:
stepWireM ::
    Monad m
    => Wire e (Kleisli m) a b
    -> a
    -> m (Either e b, Wire e (Kleisli m) a b)
In our case we have
m = IO
, so our type signature is simply:
stepWireM :: MyWire a b -> a -> IO (Either () b, MyWire a b)
This function takes a wire and an input value. It passes the input value to the wire and returns its result value of type
Either () b
. Along with the result it also returns a new wire. Normally you would call
stepWireM
in a loop, which performs instant after instant. This is the basic structure:
system :: MyWire Int String
system = {- ... -}
 
main :: IO ()
main = loop system
    where
    loop :: MyWire Int String -> IO ()
    loop w' = do
        (mx, w) <- stepWireM w' 15
 
        {- ... do something with mx ... -}
 
        loop w  -- loop with the new wire.

Note: Even though the FRP idea suggests it, there is no reason to run wires continuously or even regularly. You can totally have an instant depending on user input, a GUI event or network traffic, so instants can be minutes apart.

3.1 Testing wires

There is a convenient function for testing wires, which does all the plumbing for you. It's called
testWireM
:
testWireM ::
    (Show e, MonadIO m)
    => Int
    -> m a
    -> Wire e (Kleisli m) a String
    -> m ()
For wires returning a string, you can easily test them using this function. The first argument is a FPS/accuracy tradeoff. If it's 100, it will only print the output of every 100th instant. The second argument is an input generator action. At each instant, this action is run and its result is passed as input to the wire. The wire's output is then printed.
testWireM
prints the output continuously on a single line:
main :: IO ()
main = testWireM 1000 (return 15) system


4 Writing wires

4.1 Predefined wires

There are numerous predefined wires, which you can compose using the arrow interface. We will practice that with three very simple predefined wires (the type signatures are simplified for the sake of learning):

constant  :: b -> Wire e (>~) a b
identity  :: Wire e (>~) b b
countFrom :: Enum b => b -> Wire e (>~) a b
The constant function takes an output value and produces a wire which produces that value constantly. So the wire
constant 15
will output 15 constantly at every instant. In other words,
stepWireM
will return
Right 15
along with a new wire that outputs 15 again:
stepWireM (constant 15) inp
-> (Right 15, constant 15)
Note the fully polymorphic input type
a
. This basically means that the wire disregards its input, so whatever
inp
is, it is ignored. The identity wire is slightly more interesting. It has input and output of type
b
. What it does is: It simply outputs its input value at every instant:
stepWireM identity inp
-> (Right inp, identity)

Both identity and constant wires are examples of stateless wires. They don't change over time. You can see this in the stepping examples above. They always return themselves for the next instant.

The countFrom function takes a starting value and returns a wire that returns sequential values instant by instant. This is the first example of a stateful wire, because it changes over time:

stepWireM (countFrom 15) inp
-> (Right 15, countFrom 16)
 
stepWireM (countFrom 16) inp
-> (Right 16, countFrom 17)

4.2 Composing wires

The main feature of wires is that you can compose them using the arrow interface. There is a rich set of ways for composing, and you will want to use arrow notation for your convenience:

system :: MyWire a String
system =
    proc _ -> do
        c1 <- countFrom 10 -< ()
        c2 <- countFrom 20 -< ()
        identity -< printf "%d %d" (c1 :: Int) (c2 :: Int)

In applications it is common to write wires that ignore their input. For those wires you should make the input type fully polymorphic to indicate this. Running this wire produces:

stepWireM system ()
 
1st instant: Right "10 20"
2nd instant: Right "11 21"
3rd instant: Right "12 22"

Note: You can use the testWireM function with this wire. The following action will run the wire continuously printing its result at every 1000th instant:

main :: IO ()
main = testWireM 1000 (return ()) system
In the FRP context we often talk about signals. Particularly in the context of arrowized FRP (AFRP) like Netwire we talk about signal networks and signals passing through them. The system wire is your first signal network. It ignores its input signal and passes the signal
()
to the two counters (which ignore their input signals, too). It takes the output signals
c1
and
c2
and makes a formatted string out of them. Finally this string is passed to the
identity
wire. This is the last wire in the signal network system, so its output signal is the output signal of system. As a side note the identity wire behaves like returnA.

The main feature to note here is that all of the subwires in the composition evolve individually. So in the second instant, each of the two counters will have gone up by one. This alone gives you a powerful abstraction for stateful computations. The equivalent when using a state monad or mutable variables would be to have a global state value with two counter values. By having time-varying functions you can have something called local state. Each of the two counters (or as many as you use) have their own individual local state, which is the current counter value. This is way more convenient and composable than a state monad or other imperative state abstractions.

4.3 Choice

In traditional AFRP solutions like Yampa the path of a signal is fully determined by the structure of the signal network. In Netwire a signal can choose one of multiple paths by using the
case
and
if
constructs:
system =
    proc _ -> do
        c1 <- countFrom 10 -< ()
        if even c1
          then returnA -< "We don't want even c1"
          else do
              c2 <- countFrom 20 -< ()
              returnA -< printf "%d %d" (c1 :: Int) (c2 :: Int)
If the
c1
signal is even, then the wire outputs the string "We don't want even c1". Otherwise it takes the second path. Here it is important to know that the second counter will be suspended, when
c1
is even, because the
else
branch is not reached. A wire can only evolve, when it is actually reached. So in this example
c2
will run at half the speed of
c1
and the output will look like:
1st instant: "We don't want even c1"
2nd instant: "11 20"
3rd instant: "We don't want even c1"
4th instant: "13 21"
5th instant: "We don't want even c1"
6th instant: "15 22"
7th instant: "We don't want even c1"