我对Threepenny-Gui与StateT的交互有疑问。
考虑这个玩具程序,该玩具程序每次单击按钮都会在列表中添加一个“ Hi”项:
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]
现在,我希望它打印“自然数”而不是“ Hi”。我知道我可以利用以下事实:UI monad是围绕IO的包装,并且可以读取/写入到目前为止在数据库中达到的数字,但是出于教育目的,我想知道是否可以使用StateT,或通过Threepenny-gui界面访问列表的内容。
最佳答案
StateT
在这种情况下不起作用。问题是您需要计数器的状态在按钮回调的两次调用之间保持不变。由于回调(以及startGUI
)产生UI
动作,因此使用它们进行的任何StateT
计算都必须是自包含的,以便您可以调用runStateT
并利用生成的行动。
与Threepenny保持持久状态的主要方法有两种。第一个也是最直接的方法是使用UI
(这只是一个驻留在IORef
中的可变变量)来保持计数器状态。这样产生的代码非常类似于使用常规事件回调GUI库编写的代码。
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]
第二种方法是从命令式回调接口切换到
IO
提供的声明性FRP接口。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]
Reactive.Threepenny
的典型用法如下:首先,通过
Reactive.Threepenny
(或Event
,如果所选事件未包含在该模块中)从用户输入中获取Graphics.UI.Threepenny.Events
。此处,“原始”输入事件为domEvent
。然后,使用
eClick
和Control.Applicative
组合器按摩事件数据。在我们的示例中,我们将Reactive.Threepenny
分别转发为eClick
和eIncrement
,并分别设置不同的事件数据。最后,通过在事件数据中构建
eCount
(如Behavior
)或回调(通过使用bCounter
)来利用事件数据。行为有点像可变变量,除了行为的更改是由事件网络以有原则的方式指定的,而不是由代码库中散布的任意更新指定的。用于处理此处未显示的行为的有用函数是onEvent
函数,该函数使您可以将DOM中的属性绑定到行为的值。this question和Apfelmus对它的回答中提供了一个附加示例,以及有关这两种方法的更多注释。
细节:在FRP版本中,您可能要担心的一件事是
sink
是在eCount
触发的更新之前还是之后,在bCounter
中获取值。答案是,该值肯定会按预期使用旧值,因为如eIncrement
文档所述,Reactive.Threepenny
更新和回调触发具有名义上的延迟,而其他Behavior
操作则不会发生这种延迟。