HeinrichApfelmus / threepenny-gui

GUI framework that uses the web browser as a display.
https://heinrichapfelmus.github.io/threepenny-gui/
Other
437 stars 77 forks source link

Running an IO action (analogue of fromChanges/fromPoll)? #140

Closed jmickelin closed 7 years ago

jmickelin commented 7 years ago

Is there an analogue of Reactive Banana's fromChanges or fromPoll functions in threepenny-gui, or otherwise some other way to turn an IO a into a Behavior a?

I want to read data from a network resource at regular intervals, but I currently resort to using on with a timer and a callback function that manually has to run the IO action and update every affected Element. That doesn't really feel like FRP, so I think I must be missing something. What's the right way to do this?

HeinrichApfelmus commented 7 years ago

Sorry for the late reply.

At the moment, Threepenny uses a homegrown variant of FRP. I intend to replace it with Reactive Banana at some point, but this also means that I won't be adding any new functions to the Reactive.Threepenny API, as they will be taken over anyway.

Fortunately, I think you can solve your particular problem by using newEvent. The idea is to construct a Behavior from this event, and to execute the IO action and pass the result to the handler whenever the timer ticks. Something like this:

fromPoll :: MonadIO m => IO Int -> m (Behavior Int)
fromPoll m = liftIO $ do
    a <- m
    (e, h) <- newEvent
    onTimer $ m >>= h
    stepper a e

Does this help?

jmickelin commented 7 years ago

Hi, thanks for the reply!

I see the principle, but what would onTimer be here? I can't seem to find such a function in the library, and when trying to define it on my own I run into problems because onEvent (tick someTimer) and pals return values wrapped inside UI and not IO as needed.

HeinrichApfelmus commented 7 years ago

You should be able to use the register function from Reactive.Threepenny, like this

register (tick someTimer) $ m >>= h

The onEvent function would work as well, though you would have to play around with liftIO.

jmickelin commented 7 years ago

Oh, must've missed that one. Yep, that works! Thanks!

HeinrichApfelmus commented 7 years ago

Glad I could help. :smile: Closing.

jmickelin commented 7 years ago

Coming back to this today, I noticed that while it works fine for timers with small intervals, it breaks down with large ones. The cut-off point seems to be at 2147484 ms (around 35 minutes 47 seconds), after which the IO action keeps running without any delay at all.

Minimal example:

import           Control.Monad
import           Control.Monad.IO.Class
import           Graphics.UI.Threepenny.Core
import qualified Graphics.UI.Threepenny      as UI
import           Reactive.Threepenny

fromPoll :: MonadIO m => UI.Timer -> IO a -> m (Behavior a)
fromPoll pollTimer m = liftIO $ do
  d <- m
  (e, h) <- newEvent
  register (UI.tick pollTimer) $
    \_ -> m >>= h
  stepper d e

seconds, minutes, hours, days :: Int
seconds = 1000 -- 1000 milliseconds
minutes = 60 * seconds
hours = 60 * minutes
days = 24 * hours

setup :: Window -> UI ()
setup window = void $ do
  t <- UI.timer # set UI.interval (35 * minutes + 47 * seconds + 484)
  _ <- fromPoll t $
    do
      putStrLn $ "spam"
  UI.start t

main :: IO ()
main = startGUI defaultConfig setup

I'm also noticing some kind of intermittent fault for smaller values, where the action is run exactly twice, but I can't reproduce it reliably.

Do you have any idea what could be causing this? Should I file a separate bug?

I'm using the version from the lts-8.0 snapshot on Stackage.

jmickelin commented 7 years ago

System information:

$ uname -a
Linux datamaskinen 4.9.8-1-ARCH #1 SMP PREEMPT Mon Feb 6 13:18:39 CET 2017 i686 GNU/Linux

Will try it on a 64-bit machine as soon as I can (though 2147484 feels a bit too small to cause any overflows anyway).

massudaw commented 7 years ago

I believe is overflowing 2^31 in miliseconds. 2147484000 is quite near the Max integer value.

jmickelin commented 7 years ago

You're right, I hadn't noticed that. But the argument to set UI.interval should already be in milliseconds, according to the documentation.

jmickelin commented 7 years ago

Wait, I found the culprit! Turns out it converts it to microseconds for use with threadDelay, making it overflow as you suggested.

-- | Create a new timer
timer :: UI Timer
timer = liftIO $ do
    tvRunning     <- newTVarIO False
    tvInterval    <- newTVarIO 1000
    (tTick, fire) <- newEvent

    forkIO $ forever $ do
        atomically $ do
            b <- readTVar tvRunning
            when (not b) retry
        wait <- atomically $ readTVar tvInterval
        fire ()
        threadDelay (wait * 1000) -- <-- Here's the problem

    let tRunning  = fromTVar tvRunning
        tInterval = fromTVar tvInterval 

    return $ Timer {..}

I guess I'll just have to stay below 35 minutes on my 32-bit machine, then...

Thanks!

sjakobi commented 7 years ago

I guess I'll just have to stay below 35 minutes on my 32-bit machine, then...

I guess we could modify timer to make multiple calls to threadDelay when wait gets too big.