Synchronous Events

It turns out that there’s a certain class of event transformers (Event a -> Event b) in Reactive that have some really neat and convenient properties. We’ll call this class synchronous event transformers.

The event transformers we’re interested in (f : Event a -> Event b) semantically have the following property.

 \forall e \in \text{Event } a, \text{pfst }(f e) == \text{pfst }e

where

pfst = map fst

In other words, synchronous event transformers do not change the number of occurrences nor the times of occurrences of the argument. Most of Reactive’s event transformers have this property. Lets make a simple wrapper for Synchronous transformers:

type EventT a b = Event a -> Event b
newtype Synchronous a b = S {toEventT :: (EventT a b)}

There may be a better representation. Conal Elliott on #haskell suggested using MutantBots, but they aren’t expressive enough to encapsulate transformers like withNextE. For the purpose of this discussion, we’ll use the above representation and invite the reader to come up with a better one.

Some helper functions will come in handy. By the way, we’ll be using the style of Semantic Editor Combinators.

inS :: ((EventT a b) -> (EventT c d)) -> (Synchronous a b -> Synchronous c d)
inS f =  S . f . toEventT
 
inS2 :: ((EventT a b) -> (EventT c d) -> (EventT e f)) ->
        (Synchronous a b -> Synchronous c d -> Synchronous e f)
inS2 f (S a) (S b) = S (f a b)

An easy thing to see is that our Synchronous Event Transformers (SET’s) are Functors. The definition of fmap simply applies the function to the resultant event.

instance Functor (Synchronous a) where
  fmap = inS . fmap . fmap

One interesting thing about synchronous events is that we can apply one to another in parallel. In other words, if we have an event of functions and an event of values, we can apply each function event to each value event if they are synchronous.

-- Only works on events that are synchronous
apSync ::  EventG t (a->b) -> EventG t a -> EventG t b
apSync = inEvent2 f
  where
    f = inFuture2 (\(ta,a) (_, b) -> (ta, g a b))
    g (Stepper a ae) (Stepper b be) = (Stepper (a b) (apSync ae be))

If we have two synchronous event transformers, how can we combine them? One way would be to use apSync on their results:

apSyncT :: Synchronous e (d -> f) -> Synchronous e d -> Synchronous e f
apSyncT = inS2 (\f g e -> apSync (f e) (g e))

Hey, that looks mighty familiar:

instance Applicative (Synchronous a) where
  pure = S . pure . pure
  (<*>) = apSyncT

Interesting. Now we have an Applicative instance for synchronous event transformers. Later on, we’ll see how this is useful. As it turns out we can declare Category and Arrow instances as well:

instance Category Synchronous where
  id = S id
  (.) = inS2 (.)
 
instance Arrow Synchronous where
  arr = S . fmap
  (&&&) = liftA2 (,)
  -- Defined in terms of (&&&)
  first f = (f . arr fst) &&& arr snd

As with any applicative functor, we can define forwarded Num instances

-- Boilerplate taken from Reactive
noOv :: String -> String -> a
noOv ty meth = error $ meth ++ ": No overloading for " ++ ty
 
noFun :: String -> a
noFun = noOv "behavior"
 
-- Eq & Show are prerequisites for Num, so they need to be faked here
instance Eq (Synchronous a b) where
  (==) = noFun "(==)"
  (/=) = noFun "(/=)"
 
instance Show (Synchronous a b) where
  show      = noFun "show"
  showsPrec = noFun "showsPrec"
  showList  = noFun "showList"
 
instance Num b => Num (Synchronous a b) where
  negate      = fmap negate
  (+)         = liftA2 (+)
  (*)         = liftA2 (*)
  fromInteger = pure . fromInteger
  abs         = fmap abs
  signum      = fmap signum

Uses

Now that we have all these properties of synchronous events, how can we use them? Lets consider the types of a couple functions in reactive

withTimeE :: Ord t => EventG (Improving t) d -> EventG (Improving t) (d, t)
withTimeE_ :: Ord t => EventG (Improving t) d -> EventG (Improving t) t

Several Synchronous event functions come in pairs like this. The more general version passes along the data of the originating event and the convenient underscore variant doesn’t. The latter is implemented in terms of the former. With Synchronous, we can implement the more complex version in terms of the simpler version:

timeE :: Synchronous a TimeT
timeE = S withTimeE_
 
timeE' ::  Synchronous a (a, TimeT)
timeE' = liftA2 (,) id timeE
-- or, using Beelsebob's Applicative infix library
-- timeE' = id <^(,)^> timeE
-- or, if we make a default instance (for AFs) for Zip in Conal's TypeCompose
-- timeE' = zip id timeE

Synchronous eliminates the need to have two versions of these functions and saves us the strain of having implement the more complex variants.

More Synchronous Fun

Lets pull some more synchronous event transformers into our Synchronous data type:

prevE, nextE :: Synchronous a a
prevE = S withPrevE_
  where
    withPrevE_ = (fmap.fmap) snd withPrevE
nextE = S withNextE_
  where
    withNextE_ = (fmap.fmap) snd withNextE
 
mealyE ::  b -> (b -> b) -> Synchronous a b
mealyE a b = S (mealy_ a b)
 
splitE' ::  Event b -> Synchronous a (Event b)
splitE' = S . splitE_
  where
    splitE_ :: (Ord t) => EventG t b -> EventG t a -> EventG t (EventG t b)
    splitE_ = (fmap.fmap.fmap) snd splitE

Recall our elapsedFirstTry function from the event tutorial.

elapsedFirstTry :: Event () -> Event TimeT
elapsedFirstTry e = fmap f (withPrevE (withTimeE_ e))
  where
    f (tCur, tPrev) = tCur - tPrev

Since elapsed is a synchronous event transformer, we no longer need to implement a more complex variant of it. Thinking in terms of synchronous event transformers, we can further simplify the above definition:

elapsed ::  Synchronous a TimeT
elapsed = timeE - (prevE . timeE)

This can be read as “the time minus the previous time” for each occurrence. Our metronome implementation also sheds some complexity.

-- Old definition
metronome :: BellMachine
metronome = switchE . fmap f . withTimeE . mealy Idle next . withElapsed_
  where
    f ((dt,PlayingCycle), t) = period dt t
    f _ = mempty

to

metronome :: BellMachine
metronome = switchE . toEventT e'
  where
    e' = f <$> mealyE Idle next <*> timeE <*> elapsed
      where
        f PlayingCycle t dt = period dt t
        f _ _ _ = mempty

Using applicative in this case relieves us from tuple juggling and allows us to express more directly what we’d like. Note that toEventT was required here because switchE is not a synchronous transformer.

Arrows

I’m not certain it is all that useful, but arrows allow us to use the special arrow syntax. We can rewrite e' this way:

e' ::  Synchronous a (Event ())
e' = proc e -> do
  m <- mealyE Idle next -< e
  t <- timeE -< e
  dt <- elapsed -< e
  returnA -< if m == PlayingCycle then period t dt
                                  else mempty

– David Sankel @ Anygma.

Leave a comment

Your comment