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.
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:
Event
from user input through Graphics.UI.Threepenny.Events
(or domEvent
, if your chosen event is not covered by that module). Here, the "raw" input event is eClick
.Control.Applicative
and Reactive.Threepenny
combinators. In our example, we forward eClick
as eIncrement
and eCount
, setting different event data in each case. Behavior
(like bCounter
) or a callback (by using onEvent
) out of it. A behavior is somewhat like a mutable variable, except that changes to it are specified in a principled way by your network of events, and not by arbitrary updates strewn through your code base. An useful function for handling behaviors not shown here is sink
function, which allows you to bind an attribute in the DOM to the value of a behavior.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.