haskellfrp

Reactive's Monad and Applicative instances in Conal Elloit's FRP


Conal Elliott's paper has the following definitions:

Future a = (Time, a)
Stepper :: a -> Event a -> Reactive a
Ev :: Future (Reactive a) -> Event a
never : Event a
instance Monad Reactive
rA :: Reactive String
rA = "IA" `Stepper` (Ev (1, "1A" `Stepper` never))

rB :: Reactive String
rB = "IB" `Stepper` (Ev (1, "1B" `Stepper` never))

rC1 :: Reactive String
rC1 = (++) <$> rA <*> rB
rC1 = "IAIB" `Stepper` (Ev (1, "IA1B" `Stepper` never))

I believe the above is correct.

rC2 :: Reactive String
rC2 = rA >>= (\a -> (a++) <$> rB)

Should rC1 = rC2?

Based on the definition in the paper it appears that "IA1B" and "1AIB" would be included in between "IAIB" and "1A1B" in rC2.

Doesn't that violate a Monad law (<*>) = ap? Shouldn't rC1 = rC2? Or I am misunderstanding something.

Here is the Idris code I am using


Solution

  • There's two bugs in this story. The example given by the question actually only exposes the first one, but while working through it I found another more serious bug in the definition of join.

    The first bug is minor: it is about handling simultaneous events. Your race function allows you to merge simultaneous events. This is taken advantage of in (<*>), but then in your definition of join, the These case pretends that the left (inner) event happens first. So if you think of implementing (<*>) using join, and the two behaviors have simultaneous events, join will not merge them.

    In the case of reactive behaviors, two simultaneous events are undistinguishable from two events that happen very close to each other in time, and in any case, the last one wins, so it should not matter whether the previous one appears in the stream or not. This would break down if you start counting events for example, and from that I would simply conclude that such an operation should not be allowed on behaviors.

    I think the = symbol that appears in laws is often taken too syntactically (if you allow me to call it so), whereas there are various situations, such as this, where it's just more flexible to read it semantically. Often they coincide, so it's also hard to really see the point unless you're used to think about it formally.

    From that point of view, it would be reasonable to ignore this as "not a bug". It still seems possible to fix the implementation so that the laws do hold syntactically; that would make everyone happy at any rate.


    To understand the second bug, you must take another look at the meaning of join :: Reactive (Reactive a) -> Reactive a.

    A r :: Reactive (Reactive a) is an evolving behavior. At some time you get behavior x (whose inner variations are not shown), then behavior y, then behavior z... So schematically it looks like a stream of behaviors

    xxxxx...
    yyyyy...
    zzzzz...
    ...
    

    If we think of behaviors as functions of time r :: Time -> (Time -> a), join samples r at time t, that's another behavior r t :: Time -> a, which itself gets sampled at time t: (join r) t = r t t. In other words, you take the diagonal behavior:

    x
     y
      z
    

    Thus, if we look back to the event-driven definition of Reactive, it is only natural to forget events as soon as a new behavior appears. And indeed, the definition of join as it appears in the paper does that by racing with join <$> urr, which forgets about the inner behavior ur. However that only accounts for one side of the diagonal:

    x
    yy
    zzz
    

    Because Reactive behaviors are streams of events, inner streams of a Reactive (Reactive a) can contain events that occur before the stream's own appearance. Thus, when you get a Future (Reactive a), you also need a way to truncate events from before the Future happens:

    actualize :: Future (Reactive a) -> Future (Reactive a)
    -- Exercise for the reader
    

    and use that somewhere in join.