3

I am using WxHaskell to graphically show the state of a program that advertises state updates using TCP (which I decode using Data.Binary). When an update is received, I want to update the display. So I want the GUI to update its display asynchronously. I know that processExecAsync runs a command line process asynchronously, but I don't think this is what I want.

Alex
  • 1,581
  • 2
  • 18
  • 31
  • Can you clarify your question. What exactly are you seeking? A model for notifying a Haskell process from a separate process? – Don Stewart Jul 05 '10 at 01:09
  • Here is an example. In a separate process, there is a counter. Every time that the counter is incremented, it sends a message via TCP to other Haskell processes (the clients). The clients manage a gui (in WxHaskell) that displays the value of the counter. When an update is received by the client, I want to update the counter on the display. – Alex Jul 05 '10 at 08:42
  • Based on your comment I have posted an answer. What concepts, if any, in my answer relate to your question? Haskell threads (forkIO)? Communication between threads (MVars, STM/TVars)? Something else or not in my answer? – Thomas M. DuBuisson Jul 05 '10 at 18:45

3 Answers3

2

This is rough code using transactional variables (i.e. software transactional memory). You could use an IORef, MVar, or numerous other constructs.

main = do
    recvFunc <- initNetwork
    cntTV <- newTVarIO 0
    forkIO $ threadA recvFunc cntTV
    runGUI cntTV 0

Above you start the program, initialize the network and a shared variable cntTV

threadA recvCntFromNetwork cntTVar = forever $ do
    cnt <- recvCntFromNetwork
    atomically (writeTVar cntTVar cnt)

threadA receives data from the network and writes the new value of the counter to the shared variable.

runGUI cntTVar currentCnt = do
    counter <- initGUI
    cnt <- atomically $ do
        cnt <- readTVar cntTVar
        if (cnt == currentCnt)
            then retry
            else return cnt
    updateGUICounter counter cnt
    runGUI cntTVar cnt

runGUI reads the shared variable and if there is a change will update the GUI counter. FYI, the runGUI thread won't wake up on retry until cntTVar is modified, so this isn't a CPU hogging polling loop.

In this code I've assumed you have functions named updateGUICounter, initGUI, and initNetwork. I advise you use Hoogle to find the location of any other functions you don't already know and learn a little about each module.

Thomas M. DuBuisson
  • 64,245
  • 7
  • 109
  • 166
  • Thanks for your answer. This is sort of what I had in mind. Unfortunately, we need to call the wxHaskell function `start` which starts the event loop. If we run `start runGUI` then the event loop will never be started. And if we `forkIO` the STM stuff then the GUI is not updated (at least when I tried). – Alex Jul 05 '10 at 20:48
1

I have come up with a kind of hack that seems to work. Namely, use an event timer to check an update queue:

startClient :: IO (TVar [Update])
startClient = /*Connect to server, 
                listen for updates and add to queue*/

gui :: TVar [Update] -> IO ()
gui trdl = do
  f <- frame [text := "counter", visible := False]
  p <- panel f []
  st <- staticText p []
  t <- timer f [interval := 10, on command := updateGui st]
  set f [layout := container p $ fill $ widget st, clientSize := (sz 200 100), visible := True]
 where
   updateGui st = do
             rdl <- atomically $ readTVar trdl
             atomically $ writeTVar trdl []
             case rdl of
               [] -> return ()
               dat : dl -> set st [text := (show dat)]

main :: IO ()
main = startClient >>= start gui

So a client listens for the updates on the TCP connection, adds them to a queue. Every 10ms, an event is raised whose action is to check this queue and show the latest update in a static text widget.

If you have a better solution, please let me know!

Alex
  • 1,581
  • 2
  • 18
  • 31
  • From what I can tell there isn't any reason gui can't retry on the TVar and do this async (but yes, wx breaks seemingly any time the thread blocks). I also tried inverting the concept and writing `\str -> set st [text := str]` into the TVar then having `startClient` call this to update the GUI, but that seems to block indefinitely on `set`. WX has proven rather frustrating so I think I'll stick with GTK as a result. – Thomas M. DuBuisson Jul 06 '10 at 18:53
0

I found a solution without a busy wait at: http://snipplr.com/view/17538/

However you might choose a higher eventId in order to avoid conflicts with existing ids.

Here is some code from my module http://code.haskell.org/alsa/gui/src/Common.hs:

myEventId :: Int
myEventId = WXCore.wxID_HIGHEST+100
    -- the custom event ID, avoid clash with Graphics.UI.WXCore.Types.varTopId

-- | the custom event is registered as a menu event
createMyEvent :: IO (WXCore.CommandEvent ())
createMyEvent =
   WXCore.commandEventCreate WXCore.wxEVT_COMMAND_MENU_SELECTED myEventId

registerMyEvent :: WXCore.EvtHandler a -> IO () -> IO ()
registerMyEvent win io =
   WXCore.evtHandlerOnMenuCommand win myEventId io


reactOnEvent, reactOnEventTimer ::
   SndSeq.AllowInput mode =>
   Int -> WX.Window a -> Sequencer mode ->
   (Event.T -> IO ()) ->
   IO ()
reactOnEvent _interval frame (Sequencer h _) action = do
   mvar <- MVar.newEmptyMVar

   void $ forkIO $ forever $ do
      MVar.putMVar mvar =<< Event.input h
      WXCore.evtHandlerAddPendingEvent frame =<< createMyEvent

   registerMyEvent frame $
      MVar.takeMVar mvar >>= action

-- naive implementation using a timer, requires Non-Blocking sequencer mode
reactOnEventTimer interval frame sequ action =
   void $
   WX.timer frame [
      WX.interval := interval,
      on command  := getWaitingEvents sequ >>= mapM_ action]

The code shows two ways to handle the problem:

  • reactOnEventTimer does a busy wait using the WX timer.
  • reactOnEvent only gets active when an event actually arrives. This is the prefered solution.

In my example I wait for ALSA MIDI sequencer messages. The Event.input call waits for the next ALSA message to come. The action gets the results of Event.input, that is, the incoming ALSA messages, but it is run in the WX thread.

Lemming
  • 577
  • 6
  • 16
  • 1
    Welcome to StackOverflow. Whilst this may answer the question, [it would be preferable](http://meta.stackexchange.com/questions/8231/are-answers-that-just-contain-links-elsewhere-really-good-answers/8259#8259) to include the essential parts of the answer here, and provide the link for reference. – Chris Sep 26 '12 at 07:33