4

我想这个问题的总体答案会将我推向函数响应式编程,但是……请耐心等待。

我也没有这个问题的示例代码。我用我的一些代码在这个主题附近徘徊,但我一直牢牢地呆在 IO 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 的操作时,这些数据不会显示在视图中。我能想到让它显示在视图中的唯一方法是发送一条消息listStoreInsert my_new_event,除了对 monad 所做的更改之外。这是可行的,但开始感到笨拙。

更糟糕的是,这个神秘的树视图是一个管理视图!它是可编辑的!管理员说“哦,该事件有一些无效数据,我想更改它!”。现在,我可以毫无问题地更改上面创建的 ListStore 中的数据。我可以创建使更新毫无问题的回调。但我根本想不出如何将更新放入 Global AppState Monad。

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

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

所以...

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

4

1 回答 1

1

您无法使用正常的状态单子可靠地共享状态。如果(人为的示例)您的用户通过 GUI 编辑模型并且您同时从其他地方获得了新条目怎么办?在那种情况下,您不可能使用一些纯 monad 堆栈来序列化对 state monad 的更改。

您可以做的是使用某种使用可变引用的同步系统(MVar例如,使用 s );您将实际的应用程序状态存储MVarMVar. 这是一些伪代码,说明了我的意思:

-- 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

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

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

于 2012-07-11T14:57:19.033 回答