2012-07-11 37 views
4

我可以想象,对此的整体回答将推动我进入Functional Reactive Programming,但是...忍受着我一段时间。是否可以在GTK和Haskell中为State monad中的数据创建TreeModel?

我也没有这个问题的示例代码。我用我的一些代码在主题附近徘徊,但我一直坚守在它的IO monad中。

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

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

(假设数据存储,EventStream和套接字是所有的数据类型基本上做什么,他们听起来像:))

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

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

更糟糕的是,这个神话般的树视图是一个管理视图!这是可编辑!管理员说:“哦,那个事件有一些无效的数据,我想改变它!”。现在,我无法更改上面创建的ListStore中的数据。我可以创建回调,使更新没有问题。但我无法想象如何将更新导入Global AppState Monad。

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

即使我能想出如何创建自己的完全自定义的ModelView实例,该实例向我的AppState提供只读视图,但我无法想象如何使状态更新可用于其他应用程序。

所以......

它甚至有可能以这种方式来建模GTK /哈斯克尔应用?或者,我是否走上了疯狂之路?

回答

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单子写addToTheStatehandleChanges,就像以前那样。

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

+0

我认为你的例子是非常现实的(即没有人为的)。我完全没有想过使用MVar。这可能是一个相当有趣的解决方案。 – 2012-07-11 15:10:23

+0

我正在用手机写这一切,所以请原谅这些混乱的解释。之前我输入的一个例子更具人为性,涉及回调等不同的线程,但我意识到即使在具有不透明事件处理程序的单个线程上使用事件轮询时也需要解决相同的问题。 – dflemstr 2012-07-11 15:55:35

+0

周末我开始玩这个解决方案。我喜欢。不禁感慨我插入了一个额外的间接层,但我希望能够解决这个问题,因为我在Haskell上做得更好。 – 2012-07-23 21:18:34