State-dependent event processing with state updates
Asked Answered
M

2

6

I want to use FRP (i.e., reactive banana 0.6.0.0) for my project (a GDB/MI front-end). But I have troubles declaring the event network.

There are commands from the GUI and there are stop events from GDB. Both need to be handled and handling them depends on the state of the system.

My current approach looks like this (I think this is the minimum required complexity to show the problem):

data Command = CommandA | CommandB
data Stopped = ReasonA  | ReasonB
data State = State {stateExec :: Exec, stateFoo :: Int}
data StateExec = Running | Stopped

create_network :: NetworkDescription t (Command -> IO ())
create_network = do
    (eCommand, fCommand) <- newEvent
    (eStopped, fStopped) <- newEvent
    (eStateUpdate, fStateUpdate) <- newEvent

    gdb <- liftIO $ gdb_init fStopped

    let
      eState = accumE initialState eStateUpdate
      bState = stepper initialState eState

    reactimate $ (handleCommand gdb fStateUpdate <$> bState) <@> eCommand
    reactimate $ (handleStopped gdb fStateUpdate <$> bState) <@> eStopped

    return fCommand

handleCommand and handelStopped react on commands and stop events depending on the current state. Possible reactions are calling (synchronous) GDB I/O functions and firing state update events. For example:

handleCommand :: GDB -> ((State -> State) -> IO ()) -> State -> Command -> IO ()
handleCommand gdb fStateUpdate state CommandA = case stateExec state of
   Running -> do
     gdb_interrupt gdb
     fStateUpdate f
 where f state' = state' {stateFoo = 23}

The problem is, when f gets evaluated by accumE, state' sometimes is different from state.

I am not 100% sure why this can happen as I don't fully understand the semantics of time and simultaneity and the order of "reactimation" in reactive banana. But I guess that state update functions fired by handleStopped might get evaluated before f thus changing the state.

Anyway, this event network leads to inconsistent state because the assumptions of f on the "current" state are sometimes wrong.

I have been trying to solve this problem for over a week now and I just cannot figure it out. Any help is much appreciated.

Multivalent answered 2/8, 2012 at 13:45 Comment(1)
There seems to be a dangling parenthesis in your last line. where f state' ....Rhodium
R
3

It looks like you want to make a eStateUpdate event occur whenever eStop or eCommand occurs?

If so, you can simply express it as the union of the two events:

let        
    eStateUpdate = union (handleCommand' <$> eCommand)
                         (handleStopped' <$> eStopped)

    handleCommand' :: Command -> (State -> State)
    handleStopped' :: Stopped -> (State -> State)

    eState = accumE initialState eStateUpdate

    etc.

Remember: events behave like ordinary values which you can combine to make new ones, you're not writing a chain of callback functions.

The newEvent function should only be used if you want to import an event from the outside world. That's the case for eCommand and eStopped, as they are triggered by the external GDB, but the eStateUpdate event seems to be internal to the network.


Concerning behavior of your current code, reactive-banana always does the following things when receiving an external event:

  1. Calculate/update all event occurrences and behavior values.
  2. Run the reactimates in order.

But it may well happen happen that step 2 triggers the network again (for instance via the fStateUpdate function), in which case the network calculates new values and calls the reactimates again, as part of this function call. After this, flow control returns to the first sequence of reactimates that is still being run, and a second call to fStateUpdate will have strange effects: the behaviors inside the network have been updated already, but the argument to this call is still an old value. Something like this:

reactimate1
reactimate2
    fStateUpdate      -- behaviors inside network get new values
        reactimate1'
        reactimate2'
reactimate3           -- may contain old values from first run!

Apparently, this is tricky to explain and tricky to reason about, but fortunately unnecessary if you stick to the guidelines above.


In a sense, the latter part embodies the trickiness of writing event handlers in the traditional style, whereas the former part embodies the (relative) simplicity of programming with events in FRP-style.

The golden rule is:

Do not call another event handler while handling an event.

You don't have to follow this rule, and it can be useful at times; but things will become complicated if you do that.

Rhodium answered 3/8, 2012 at 8:38 Comment(5)
Heinrich, thank you for helping me. I understand your guidelines but I don't see how to apply them in my case because handleCommand needs to perform IO actions. For example, in order to handle an interrupt command from the GUI handleCommand needs to call gdb_interrupt first and then gdb_backtrace to see where the execution got interrupted. This information is then required to update the state accordingly.Multivalent
Ah, I see. IO actions are problematic, as usual. The important thing that I want to point out here is that once you deal with IO actions, you will soon run into problems of ordering, regardless of whether you employ FRP or not. For instance, it is conceivable that the gdb_backtrace function generates another event and then the question is: do you update the state before handling it or after handling it, or do you handle it at all?Rhodium
However, if I understand that correctly, gdb_backtrace is essentially a pure operation that just queries the internal state of the debugger, and hence should be safe to call at any time. You're probably looking for a function unsafeMapIO :: Event t (IO a) -> Event t a, but reactive-banana doesn't currently support that and I'm not entirely sure whether it is really a good idea.Rhodium
I'm afraid I don't have a ready-made answer for your impedance mismatch between FRP/pure and GDB/IO. However, I have a hunch that making the internal state of the GDB debugger explicit as a Behavior DebuggerState may help a lot; in particular, you can perform arbitrary of IO actions from inside an AddHandler and use fromChanges to get the debugger state.Rhodium
Thank you, Heinrich. Good to know that it's indeed the nature of the problem that makes things complex. I am not sure either, what feature would help as I am not familiar with FRP yet. I will answer to this question once I found a viable solution.Multivalent
M
1

As far as I can see, FRP seems not to be the right abstraction for my problem.

So I switched to actors with messages of type State -> IO State.

This gives me the required serialization of events and the possibility to do IO when updating the state. What I loose is the nice description of the event network. But it's not too bad with actors either.

Multivalent answered 6/8, 2012 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.