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 (andstartGUI
as well) produceUI
actions, anyStateT
computation to be ran using them has to be self-contained, so that you can callrunStateT
and make use of the resultingUI
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 inIO
) to hold the counter state. That results in code much like that written with conventional event-callback GUI libraries.The second way is switching from the imperative callback interface to the declarative FRP interface provided by
Reactive.Threepenny
.Typical usage of
Reactive.Threepenny
goes like this:Event
from user input throughGraphics.UI.Threepenny.Events
(ordomEvent
, if your chosen event is not covered by that module). Here, the "raw" input event iseClick
.Control.Applicative
andReactive.Threepenny
combinators. In our example, we forwardeClick
aseIncrement
andeCount
, setting different event data in each case.Behavior
(likebCounter
) or a callback (by usingonEvent
) 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 issink
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 inbCounter
before or after the update triggered byeIncrement
. The answer is that the value will surely be the old one, as intended, because, as mentioned by theReactive.Threepenny
documentation,Behavior
updates and callback firing have a notional delay that does not happen with otherEvent
manipulation.