Personal tools

Netwire

From HaskellWiki

Revision as of 17:59, 1 December 2011 by Ertes (Talk | contribs)

Jump to: navigation, search

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 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.1 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. Side note: The identity wire behaves like returnA.

4.2 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
:
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)