haskellmonad-transformersstate-monadthreepenny-gui

Mixing Threepenny-Gui and StateT


I have a question on the interaction of Threepenny-Gui with StateT. Consider this toy program that, every time the button is clicked, adds a "Hi" item in the list:

import           Control.Monad
import           Control.Monad.State

import qualified Graphics.UI.Threepenny      as UI
import           Graphics.UI.Threepenny.Core hiding (get)

main :: IO ()
main = startGUI defaultConfig setup

setup :: Window -> UI ()
setup w = void $ do
  return w # set title "Ciao"
  buttonAndList <- mkButtonAndList
  getBody w #+ map element buttonAndList

mkButtonAndList :: UI [Element]
mkButtonAndList = do
  myButton <- UI.button # set text "Click me!"
  myList <- UI.ul
  on UI.click myButton $ \_ -> element myList #+ [UI.li # set text "Hi"]
  return [myButton, myList]

Now, instead of "Hi", I'd like it to print the natural numbers. I know that I could use the fact that the UI monad is a wrapper around IO, and read/write the number I reached so far in a database, but, for educational purposes, I'd like to know if I can do it using StateT, or otherwise accessing the content of the list via Threepenny-gui interface.


Solution

  • StateT won't work in this case. The problem is that you need the state of your counter to persist between invocations of the button callback. Since the callback (and startGUI as well) produce UI actions, any StateT computation to be ran using them has to be self-contained, so that you can call runStateT and make use of the resulting UI action.

    There are two main ways to keep persistent state with Threepenny. The first and most immediate is using an IORef (which is just a mutable variable which lives in IO) to hold the counter state. That results in code much like that written with conventional event-callback GUI libraries.

    import           Data.IORef
    import           Control.Monad.Trans (liftIO)
    
    -- etc.
    
    mkButtonAndList :: UI [Element]
    mkButtonAndList = do
      myButton <- UI.button # set text "Click me!"
      myList <- UI.ul
    
      counter <- liftIO $ newIORef (0 :: Int) -- Mutable cell initialization.
    
      on UI.click myButton $ \_ -> do
        count <- liftIO $ readIORef counter -- Reads the current value.
        element myList #+ [UI.li # set text (show count)]
        lift IO $ modifyIORef counter (+1) -- Increments the counter.
    
      return [myButton, myList]
    

    The second way is switching from the imperative callback interface to the declarative FRP interface provided by Reactive.Threepenny.

    mkButtonAndList :: UI [Element]
    mkButtonAndList = do
      myButton <- UI.button # set text "Click me!"
      myList <- UI.ul
    
      let eClick = UI.click myButton  -- Event fired by button clicks.
          eIncrement = (+1) <$ eClick -- The (+1) function is carried as event data.
      bCounter <- accumB 0 eIncrement -- Accumulates the increments into a counter.
    
      -- A separate event will carry the current value of the counter.
      let eCount = bCounter <@ eClick
      -- Registers a callback.
      onEvent eCount $ \count ->
        element myList #+ [UI.li # set text (show count)]
    
      return [myButton, myList]
    

    Typical usage of Reactive.Threepenny goes like this:

    An additional example, plus some more commentary on the two approaches, is provided in this question and Apfelmus' answer to it.


    Minutiae: one thing you might be concerned about in the FRP version is whether eCount will get the value in bCounter before or after the update triggered by eIncrement. The answer is that the value will surely be the old one, as intended, because, as mentioned by the Reactive.Threepenny documentation, Behavior updates and callback firing have a notional delay that does not happen with other Event manipulation.