reactive-banana: Firing event that contain the mos

2019-03-24 15:03发布

问题:

Suppose I have an event trigger which I want to do two things when fired. First, I want it to update the value of some behavior. Second, if other conditions are met, I want it to fire another event send_off with the updated value of the behavior. Expressed in code form, suppose I have

trigger :: Event b
trigger = ...

updateFromTrigger :: b -> (a -> a)
updateFromTrigger = ...

conditionFromTrigger :: b -> Bool
conditionFromTrigger = ...

behavior :: Behavior a
behavior = accumB initial_value (updateFromTrigger <$> trigger)

send_off :: Event a
send_off = ?????? (filterE conditionFromTrigger trigger)

Then the question is: what do I put in the ?????? so that send_off sends the most up to date value of behavior, by which I mean that the value which includes the update from trigger that was just applied to it.

Unfortunately, if I understand correctly, the semantics of Behavior are such that the updated value isn't immediately available to me, so my only option here is essentially to duplicate the work and recompute the updated value of behavior so that I can use it immediately in another event, i.e. to fill in the ?????? with something like

send_off =
    flip updateFromTrigger
    <$>
    behavior
    <@>
    filterE conditionFromTrigger trigger

Now, there is a sense in which I can make the updated information in the behavior available to me right away by using a Discrete instead of a Behavior, but really this is just equivalent to giving me an event that is fired simultaneously with my original event with the updated value, and unless I have missed something reactive-banana doesn't give me a way to fire an event only when two other events have fired simultaneously; that is, it provides unions of events but not intersections.

So I have two questions. First, is my understanding of this situation correct, and in particular am I correct in conclusion that my solution above is the only way to work around it? Second, purely out of curiosity, have there been any thoughts or plans by the developers on how to deal with intersections of events?

回答1:

Excellent question!

Unfortunately, I think that there is fundamental problem here that has no easy solution. The problem is the following: you desire the most recent accumulated value, but trigger may contain simultaneously occuring events (that are still ordered). Then,

Which of the simultaneous accumulator updates is going to be the most recent?

The point is that the updates are ordered in the event stream they belong to, but not in relation to other event streams. The FRP semantics used here no longer know which simultaneous update to the behavior corresponds to which simultaneous send_off event. In particular, this shows that your proposed implementation for send_off is likely incorrect; it doesn't work when trigger contains simultaneous events because the behavior may be updated multiple times, but you're only recalculating the update once.

With this in mind, I can think of several approaches to the problem:

  1. Use mapAccum to annotate each trigger event with the newly updated accumulator value.

    (trigger', behavior) = mapAccum initial_value $ f <$> trigger
        where
        f x acc = (x, updateFromTrigger acc)
    
    send_off = fmap snd . filterE (conditionFromTrigger . fst) $ trigger'
    

    I think that this solution is lacking a bit in terms of modularity, but in light of the discussion above, this is probably hard to avoid.

  2. Recast everything in terms of Discrete.

    I don't have any concrete suggestion here, but it may be that your send_off event feels more like an update to a value than like a proper event. In that case, it may be worth to cast everything in terms of Discrete, whose Applicative instance does "the right thing" when simultaneous events occur.

    In a similar spirit, I often use changes . accumD instead of accumE because it feels more natural.

  3. The next version of reactive-banana (> 0.4.3) will likely include functions

    collect :: Event a   -> Event [a]
    spread  :: Event [a] -> Event a
    

    that reify, resp. reflect simultaneous events. I need them to optimize Discrete type anyway, but they are probably useful for stuff like the present question as well.

    In particular, they would allow you to define the intersection of events thusly:

    intersect :: Event a -> Event b -> Event (a,b)
    intersect e1 e2
            = spread . fmap f . collect
            $ (Left <$> e1) `union` (Right <$> e2)
        where
        f xs = zipWith (\(Left x) (Right y) -> (x,y)) left right
          where (left, right) = span isLeft xs 
    

    However, in light of the discussion above, this function may be less useful than you'd like it to be. In particular, it's not unique, there are many variants.