reactive-banana
reactive-banana copied to clipboard
Clarify `switchE` boundary semantics
I think the relationship between the outer and inner events in switchE could be clarified a bit in the documentation (perhaps with another fancy graph? :D)
The semantic pseudo-code is accurate, but a bit hard to read/understand. I had to play around with Reactive.Banana.Model to clarify that:
Given events e1, e2, and event-of-events e3...
t = 0 1 2 3 ...
e1 = a b ...
e2 = x y z ...
e3 = e1 e2 ...
then e4 = switchE e3 is:
t = 0 1 2 3 ...
e4 = b z ...
So, in the case of simultaneous event occurrences, the old value is emitted before switching.
Thanks!
Would it be sensible to provide a variant with the opposite boundaries? That is, with these semantics:
switchE ee = \time0 -> concat [trim t1 t2 e | (t1,t2,e) <- intervals ee, time0 <= t1]
where
intervals e = [(time1, time2, x) | ((time1,x),(time2,_)) <- zip e (tail e)]
trim time1 time2 e = [x | (timex,x) <- e, time1 <= timex, timex < time2]
rather than these:
switchE ee = \time0 -> concat [trim t1 t2 e | (t1,t2,e) <- intervals ee, time0 <= t1]
where
intervals e = [(time1, time2, x) | ((time1,x),(time2,_)) <- zip e (tail e)]
trim time1 time2 e = [x | (timex,x) <- e, time1 < timex, timex <= time2]
(note < and <= are swapped in the definition of trim)
I am currently not aware of a fundamental reason why switchE could not be provided in the variant that you describe.
However, I do not know how to implement this variant efficiently, for the following reason:
In an efficient, push-driven implementation, events "push" their values to the events that depend on them. This requires building a dependency graph, i.e. a data structure that records which events depend on which. When an external event fires, this graph has to be traversed in dependency order. However, combinators from the dynamic event switching class, like switchE, will modify this graph. The problem is that I do not know how modify the graph while in the middle of a traversal, as this would destroy existing dependency information. The simplest solution is to make the modifications only become visible when the next external event fires, which corresponds to the time1 < timex semantics.
Okay, thank you for the clarification!
I thought of another detail that could be expounded on in the documentation regarding mutual recursion between events and behaviors.
In general, mutual recursion between several Events and Behaviors is always well-defined, as long as an Event depends on itself only via a Behavior, and vice versa.
This is not quite true, an event can actually depend on itself via an event defined using switchE, which acts like a behavior in the sense that it changes "slightly after" the event used in its definition.
One use case I discovered is keeping track of client to a sever. Say when a client connects, we identify it with an Int, and dynamically add a new Event () indicating the client has disconnected (using execute):
data Client = Client
{ clientId :: Int
, clientDisconnect :: Event ()
}
---
eClient :: Event Client <- ...
So we accumulate all of these connections...
eClients :: Event [Client] <-
accumE [] (unions
[ (:) <$> eClient -- New client connected
, {- elided for now -} -- Client disconnected
])
Then we can get an event of disconnects tagged with the client they correspond to, which depends on the currently-connected clients, of course:
let disconnect :: Client -> Event Int
disconnect c = cliendId c <$ clientDisconnect c
eDisconnect :: Event Int <-
switchE (foldr (unionWith const) never . map disconnect <$> eClients)
And now we can fill out the rest of the definition of eClients, which just filters the list every time a disconnected event fires:
eClients :: Event [Client] <-
accumE [] (unions
[ (:) <$> eClient -- New client connected
, (\i -> filter (\c -> clientId c /= i)) <$> eDisconnect -- Client disconnected
])
So we have an event eClients defined in terms of an event eDisconnect, which is itself defined in terms of eClients. And it works!
Actually I think I misspoke there, because in my example, eDisconnect does not depend directly on eClients, but rather switches whenever it emits. It seems superficially similar, but when I tried to draw the even network on paper I realized there was not actually any mutual recursion involved. So nevermind me :)
By the way, this combinator apparently exists in reflex: switchPromptly
By the way, this combinator apparently exists in reflex: switchPromptly
That's interesting. I have reason to believe that it may be buggy. When I have time, I will have to dig up the example again that convinced me that this combinator cannot be implemented efficiently.