首页 文章

是否可以在GTK和Haskell中的状态monad中为数据创建TreeModel?

提问于
浏览
4

我可以想象,对此的总体答案将推动我进行功能反应式编程,但是......请耐心等待一下 .

我也没有这个问题的示例代码 . 我和我的一些代码一起在这个主题附近徘徊,但我坚持使用IO monad .

想象一下,我有一个应用程序,我在其中建模一些复杂的状态,并将其放入一个整体的应用程序状态monad . 我这样做是因为我想要在我的核心应用程序和特定用户界面之间进行一定程度的分离 .

data S = S DataStore EventStream Sockets
type AppState m = StateT S m

(假设DataStore,EventStream和Sockets都是基本上听起来像它们的数据类型:))

现在,假设我想在GTK(TreeView,但没有子节点)中创建一个只能查看EventStream的表 . 我已经学会了这样做,说 listStoreNew event_stream >>= treeViewNewWithModel (参见http://markus.alyra.org/?p=1023我非常广泛地讨论了设置它的机制) .

但是,现在我的AppState monad中有一个可变数据副本 . 当应用程序启动并执行将新数据附加到EventStream的操作时,该操作将不会显示在视图中 . 我能想到它的唯一方法是让它显示在视图中,除了对monad所做的更改外,还会发送一条消息,如 listStoreInsert my_new_event . 这是可行的,但开始感到笨拙 .

更糟糕的是,这个神秘的树视图是一个管理视图!这是可编辑的!管理员说"oh, that event has some invalid data, I want to change it!" . 现在,我在更改上面创建的ListStore中的数据时没有任何问题 . 我可以创建回调,使更新没有问题 . 但我完全没有想到如何将更新发送到Global AppState Monad .

最后几句话显示了问题的核心 . 如果我有一个全局的AppState Monad,那么任何更新该monad的东西都必须在一行执行中,并且想要查看monad . TreeView打破了这一点 . 当在TreeView monad中编辑单元格时,编辑处理程序完全在IO monad中运行,并且不会返回任何内容 . 结束数据类型为 IO () . 即使我有一些很好的方式从我的AppState中解包数据,然后执行编辑处理程序,然后重新包装我的AppState中的数据,应用程序的其他任何分支都无法看到它 .

即使我可以弄清楚如何创建我自己的完全自定义的ModelView实例,该实例为我的AppState提供只读视图,我也想不出如何使状态更新可用于应用程序的其余部分 .

所以...

甚至可以用这种方式建模GTK / Haskell应用程序吗?或者,我是否已经走上了疯狂的道路?

1 回答

  • 1

    您无法使用普通状态monad可靠地共享状态 . 如果(设计示例)您的用户通过GUI编辑模型并且您同时从其他地方获得新条目,该怎么办?在这种情况下,您无法使用一些纯monad堆栈序列化对状态monad的更改 .

    您可以做的是使用某种使用可变引用的同步系统(例如,使用 MVar );您将实际的应用程序状态存储在 MVar 中,并且每当发生可能读取或更改状态的事件时,您都可以访问 MVar . 这是一些伪代码,显示了我的意思:

    -- This is the MVar that stores your application state
    appStateMVar :: MVar S
    appStateMVar = unsafePerformIO $ newMVar initialAppState
    {-# NOINLINE appStateMVar #-}
    -- It could also be passed as a parameter to the functions below, so that when
    -- you define the callbacks, you create a closure over the MVar that you use.
    -- (i.e.:
    -- > appStateMVar <- newMVar initialAppState
    -- > createListViewWithCallback $ whenUserAddedSomethingViaTheGUI appStateMVar
    -- )
    -- That way, you don't have to have the MVar in global scope and can avoid the
    -- use of `unsafePerformIO` to initialize it, etc.
    
    main :: IO ()
    main = do
      createListViewWithCallback whenUserAddedSomethingViaTheGUI
      createSocketsAndListenUsingCallback whenChangesArriveOverTheNetwork
      runSomeKindOfMainLoop
    
    -- This would be called on any thread by the GUI when the user added something in
    -- the view (For example)
    whenUserAddedSomethingViaTheGUI :: AddedThing -> IO ()
    whenUserAddedSomethingViaTheGUI theThingThatWasAdded =
      takeMVar appStateMVar >>=
      execStateT (addToTheState theThingThatWasAdded) >>=
      putMVar appStateMVar
    
    -- This would be called by the network when something changed there
    whenChangesArriveOverTheNetwork :: ArrivedChanges -> IO ()
    whenChangesArriveOverTheNetwork theChangesThatArrived =
      takeMVar appStateMVar >>=
      execStateT (handleChanges theChangesThatArrived) >>=
      putMVar appStateMVar
    

    然后,您可以使用纯 AppState monad编写 addToTheStatehandleChanges ,就像之前一样 .

    当然,如果您决定使用FRP,您可以通过让应用程序状态成为随时间变化的纯信号来避免这种非常强制性的状态布线 . 我知道 reactive-banana 做了一些工作,可以将双向GUI编辑器/视图与FRP事件网络集成 .

相关问题