Event handling in GTK2hs: managing events and global state

Hello community, I'm just diving into my first bigger Haskell problems, and got stuck at a certain point with gtk2hs. I couldn't find documentation or even small examples for it, no matter where I looked, so I turn to this list. My basic problem is that I want a certain action to happen based on the effects of a side effect; in my case, loading a file into a widget. Together with the file, I load a data structure, and define an event that will translate clicks on the widget into some magic on the data structure. So when I load a file, I do the following:
loadFile :: GladeXML -> FilePath -> IO () loadFile xml fp = do widget <- xmlGetWidget -- … -- do stuff with the widget, yield some 'data' connectId <- widget `on` buttonPressEvent $ handler data …
(this is a minimal trivial example, to show the crucial point.) So, the handler step depends on the particular contents of the file. This means, that for every file I load, I first have to disable the old action, and only then install a new one, since I generate a new handler every time a file is loaded. Every addition of a handler gives me a connectId, which is its true name. And, as the gtk2hs docs inform me, I have to call it by its true name to dispel it. The gtk2hs docs suggest I 'keep around' the connectId, but herein lies my problem. The loadFile function can be called from several spots in the application (and I'd like to keep it that way.) Even if I wrapped loadFile in a State monad, those entry points to load file (command line, menu, button, possibly more) have no way of knowing the state of connectId, lest I make it global (*shudders*) using unsafePerformIO and a Maybe IORef. Forgive me if I'm missing something obvious, but I have thus far only used monads for serialized code flows, and not for asynchronous, event-driven computation. So the other approach would be (and thanks to mmorrow at freenode for the idea) to wrap my widget component (if I understood correctly) in a new type that also keeps track of its connectedId (namely, the event it's been connected to) via an IORef. This way the state gets 'hidden' in the IO monad, via IORef. I'd end up with something like data WW = WW { widget :: SomeWidget , connectId :: IORef (Maybe (ConnectId SomeWidget)) } and of course no monads to indicate there's something state-changing going on (Except the IO monad, which is the underlying monad for the whole brouhaha anyway.) The above call would look more like:
main = do … widget <- xmlGetWidget … nothingRef <- newIORef Nothing openButton `onClicked` $ loadFile xml fp (WW widget nothingRef) …
loadFile :: GladeXML -> FilePath -> WW -> IO () loadFile xml fp (WW widget cid) = -- do stuff, yield some 'data' ref <- readIORef cid case ref of (Just i) -> signalDisconnect i Nothing -> return () connectId <- widget `on` buttonPressEvent $ handler data widget writeIORef cid $ Just connectId …
This doesn't strike me as a very elegant way of doing it. I'm sure there is some nice Haskellery that would make it more beautiful, so I'd like to ask this list what that might be? Also, a small plea: the gtk2hs documentation is sorely lacking an example of the usage of "signalDisconnect". I can provide a working minimal example of this solution, if anyone is interested, so maybe that could serve as a documentation. Though I still hope that this isn't the best possible way of doing it. Thanks in advance, Aleks

On 2009 Mar 25, at 0:24, Александър Л. Димитров wrote:
The gtk2hs docs suggest I 'keep around' the connectId, but herein lies my problem. The loadFile function can be called from several spots in the application (and I'd like to keep it that way.) Even if I wrapped loadFile in a State monad, those entry points to load file (command line, menu, button, possibly more) have no way of knowing the state of connectId, lest I make it global (*shudders*) using unsafePerformIO and a Maybe IORef.
You can make it "global" without using unsafePerformIO: Set up a ReaderT MyState IO and run the program inside it, after initializing your MyState with appropriate IORefs. You need to lift gtk2hs functions, and callbacks have to be wrapped to push them back up into the ReaderT: curState <- ask lift $ widget `on` buttonPressEvent $ runReaderT curState . myCallback The reason you want a ReaderT instead of a StateT is that any state not accessed via an IORef can't be propagated between the mainline code and callbacks, so you want something that is forced to be read- only after initialization except via an IORef stored within it. Aside: I've suggested at times that gtk2hs use class MonadIO instead of type IO, which would make explicit lifting (and I think "dropping") unnecessary. I never made a formal enhancement suggestion though; I should do so. -- brandon s. allbery [solaris,freebsd,perl,pugs,haskell] allbery@kf8nh.com system administrator [openafs,heimdal,too many hats] allbery@ece.cmu.edu electrical and computer engineering, carnegie mellon university KF8NH

Also Sprach Brandon S. Allbery KF8NH:
You can make it "global" without using unsafePerformIO: Set up a ReaderT MyState IO and run the program inside it, after initializing your MyState with appropriate IORefs. You need to lift gtk2hs functions, and callbacks have to be wrapped to push them back up into the ReaderT:
curState <- ask lift $ widget `on` buttonPressEvent $ runReaderT curState . myCallback
The reason you want a ReaderT instead of a StateT is that any state not accessed via an IORef can't be propagated between the mainline code and callbacks, so you want something that is forced to be read-only after initialization except via an IORef stored within it.
Aside: I've suggested at times that gtk2hs use class MonadIO instead of type IO, which would make explicit lifting (and I think "dropping") unnecessary. I never made a formal enhancement suggestion though; I should do so.
Thanks for your very helpful answer :-) I ended up passing a state object around in functions and then porting it back to use the Monad (damn deadlines, always in the way of sound programming.) If the GTK2hs people made this a bit easier, it would surely help. I'll look into the possibilities myself, too. Thanks again, Aleks
participants (2)
-
Brandon S. Allbery KF8NH
-
Александър Л. Димитров