我对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
然后,使用eClickControl.Applicative组合器按摩事件数据。在我们的示例中,我们将Reactive.Threepenny分别转发为eClickeIncrement,并分别设置不同的事件数据。
最后,通过在事件数据中构建eCount(如Behavior)或回调(通过使用bCounter)来利用事件数据。行为有点像可变变量,除了行为的更改是由事件网络以有原则的方式指定的,而不是由代码库中散布的任意更新指定的。用于处理此处未显示的行为的有用函数是onEvent函数,该函数使您可以将DOM中的属性绑定到行为的值。


this question和Apfelmus对它的回答中提供了一个附加示例,以及有关这两种方法的更多注释。



细节:在FRP版本中,您可能要担心的一件事是sink是在eCount触发的更新之前还是之后,在bCounter中获取值。答案是,该值肯定会按预期使用旧值,因为如eIncrement文档所述,Reactive.Threepenny更新和回调触发具有名义上的延迟,而其他Behavior操作则不会发生这种延迟。

07-24 12:35