Yampa's Switches

Yampa is a pretty cool arrow-based framework for writing FRP programs in Haskell. However, its documentation is sorely lacking. There is a limited amount of information on the Haskell wiki, and the Haddock documentation on Hackage lists function after function (often with obscure names) with no detail as to its use - other than the type signature. I plan to remedy this.

I have already started to expose some of the "hidden" documentation in the source code, and add some extra details where able - but there are still lots of gaps to be filled. For example, on the wiki there are a number of pages explaining a selection of Yampa's different "switch" functions - which are core to making the program "react" to events. The page regarding the standard switch is the only one with any documentation other than the type signature and a diagram. As far as the rest go, the standard switch is relatively intuitive but it's only one of a few with any documentation anywhere: in the wiki, on Hackage, or in the source code.

Preamble

In order to demonstrate these functions I will be using the commonly used falling ball example as a starting point. It's based on the example used in Jekor's "Code Deconstructed" episodes on Cuboid. In the following code, a ball (represented by the type Ball) falls through space from a standstill under the effect of gravitational acceleration (9.81m/s^2). The initial state of the ball is given by the top-level value ball. The signal function fallingBall takes the initial state and integrates the effect of gravity on the ball's velocity; and the effect of the ball's velocity on its position.

type Scalar = Double  
type Vec3 = (Scalar, Scalar, Scalar)  
type Pos = Vec3  
type Vel = Vec3  
type Acc = Vec3

data Ball = Ball { pos :: Pos, vel :: Vel } deriving (Show, Read, Eq)

-- Initial state.
ball :: Ball  
ball = Ball (0, 10, 0) (0, 0, 0)

gravityVector :: Acc  
gravityVector = (0, -9.81, 0)

-- Signal function.
fallingBall :: Ball -> SF () Ball  
fallingBall initial = proc _ -> do  
    v <- integral >>^ (^+^ v0) -< gravityVector  -- Add gravitational acceleration to velocity
    p <- integral >>^ (^+^ p0) -< v  -- Add velocity to position
    returnA -< initial { pos = p, vel = v }
    where
        v0 = vel initial
        p0 = pos initial

We will now extend this example with a signal function which makes the ball bounce when it reaches y=0. To do this, the effect of the signal function much be changed by using a switch.

In Yampa, signal functions can be swapped-out in response to events; this facilitates the "reactive" part of functional reactive programming. Switch functions create the signal functions which are able to do this. There are a few different basic switch functions: the standard switch, the recurring rSwitch, and the "call-with-current-continuation" kSwitch. There are also parallel switches which won't be discussed in this blog post. Every switch function also has a "delayed observation" counterpart which is prefixed with a d. The switching events of these functions are non-strict and their effects are not immediately observable.

We will now use each type of switch in turn to demonstrate how they work and how they differ from each other.

switch

The standard basic switch has the following type:

switch :: SF in (out, Event t)  
       -> (t -> SF in out)
       -> SF in out

If you're new to Yampa, this could be fairly intimidating but it's really quite simple. The wiki has this to say about it:

A switch in Yampa provides change of behavior of signal functions (SF) during runtime. The function 'switch' is the simplest form which can only be switched once. The signature is read like this: "Be a SF which is always fed a signal of type 'in' and returns a signal of type 'out'. Start with an initial SF of the same type but which may also return a transition event of type 'Event t'. In case of an Event, Yampa feeds the variable 't' to the continuation function 'k' which produces the new SF based on the variable 't', again with the same input and output types."

Here is an example of switch in use.

-- Get y component of a given vector.
y3 :: Vec3 -> Scalar  
y3 (x, y, z) = y

-- Flip y component of velocity to simulate fully-elastic collision.
bounce :: Ball -> Ball  
bounce (Ball p (x, y, z)) = Ball p (x, -y, z)

update :: Ball -> SF () Ball  
update initial = switch update' onBounce  
    where
        update' = proc _ -> do
            b' <- fallingBall initial -< ()  -- Apply falling signal function
            e <- edge -< y3 (pos b') <= 0  -- Detect floor and raise event
            returnA -< (b', e `tag` b')  -- Return ball and event tagged with ball

        onBounce ball' = update $ bounce ball'  -- The new signal function to switch to

kSwitch

One of Yampa's more interesting features is the ability to "freeze" a signal function and reactivate it later. kSwitch is a switch function which gives you the ability to keep the old signal function in a frozen state to use later.

kSwitch :: SF a b  -- Update  
        -> SF (a,b) (Event c)  -- Trigger based on input and output of update SF
        -> (SF a b -> c -> SF a b)  -- Generate new SF from old SF and event value
        -> SF a b

This type signature is a little more complicated, lets break it down:

  • The first argument is a signal function is the one that you want to switch
  • The second signal function acts as a trigger, reading the first signal function's input and output, and outputting an event when it wants to initiate the switch
  • The third argument is a function which generates the signal function to switch to. It takes the old signal function (frozen in state) and the event's value as its arguments

Other that having access to the old signal function, the other advantage of kSwitch over switch is that it doesn't require that the signal function being switched is kept separate from the one generating the event. The same functionality implemented above can be re-implemented with kSwitch as follows:

-- y3 and bounce as above.

update :: Ball -> SF () Ball  
update initial = kSwitch (fallingBall initial) trigger cont  
    where
        trigger = proc (_, ball') -> do
            e <- edge -< y3 (pos ball') <= 0
            returnA -< e `tag` ball'

        cont old e = update $ bounce e

rSwitch

In the source code, rSwitch is described as a "recurring switch". As opposed to switch and kSwitch, rSwitch takes a signal function and produces a modified one which, in addition to having it's usual input value also takes an event tagged with a new signal to switch to. This is the type signature:

rSwitch :: SF a b  
        -> SF (a, Event (SF a b)) b

This arrangement makes switching less flexible because the event which triggers the switch cannot come from the signal function being switched. This makes an implementation of bounce using rSwitch hard, if not impossible (I have not found a solution). However rSwitch does have its uses. For example, to cycle through a list of signal functions, change every n seconds, there is a very elegant solution using rSwitch:

cycler :: Time -> [SF a b] -> SF a b  
cycler int sfs = proc inp -> do  
    e <- afterEach $ zip (repeat int) sfs -< ()
    rSwitch (head sfs) -< (inp, e)

In this example, afterEach takes a list of (Time, a) pairs and produces an Event a after the duration specified with each value. Used in conjunction with cycle from Prelude (which loops a finite list to make an infinite list), the cycle can continue forever.

-- Cycle between three different signal functions for all time, changing very 5 seconds.
cycler 5 $ cycle [sf1, sf2, sf3]  

Another thing to note is that after the switch event, the switch is still in place - ready to accept any new signal functions. This is why it's a "recurring" switch. switch and kSwitch on the other hand require that the new signal function have its own switch defined to make the behavior repeat.

Summary

Yampa's basic switches provide three different methods of changing the running signal function in response to events:

  • The simple switch which requires the signal function being switched provide an event in addition to the normal output
  • The "call-with-current-continuation" kSwitch which has a separate "trigger" signal function, and provides the frozen state of the old signal function which can be re-used later
  • The repeating rSwitch which takes the new signal function from the event triggering the switch