I'm very slowly getting up to speed in Haskell, trying to get a gui toolgit usable, etc. I followed a basic tutorial on using glade to create a simple GUI app and now I'm trying to modularize it. In particular, I wanted to leverage functions instead of doing everything in main. The first thing I did was create separate functions for accessing buttons and associating code to be executed when the buttons are clicked. It works fine but if you look at the code below, I have to carry the entire glade XML "variable" around with me. I realize we don't do global in Haskell but it seems to me there has to be a better mechanism rather than carrying every single variable around in functions. Obviously in the OO world, the XML stuff would just be an instance variable in a class so implicitly available everywhere. What's the "right" way to do this in the Haskell world?
module Main (main) where
import Graphics.UI.Gtk
import Graphics.UI.Gtk.Glade
getButton :: GladeXML -> String -> IO Button
getButton gladeXML buttonName =
xmlGetWidget gladeXML castToButton buttonName
onButtonClick :: GladeXML -> String -> [IO a] -> IO ()
onButtonClick gladeXML buttonName codeSequence = do
aButton <- getButton gladeXML buttonName
_ <- onClicked aButton $ do -- Run the sequence of operations when user clicks
sequence_ codeSequence
return ()
loadGladeFile :: FilePath -> IO (Maybe GladeXML)
loadGladeFile filename = do
g <- xmlNew filename
return g
main :: IO ()
main = do
_ <- initGUI -- Setup
-- Load the Glade XML file
Just xml <- loadGladeFile "tutorial.glade"
-- Create main window (everything inside will be created too)
window <- xmlGetWidget xml castToWindow "window1"
-- Define what to do when we quit
_ <- onDestroy window mainQuit
-- Show the wondow
widgetShowAll window
-- Associate an onClick event with a button
onButtonClick xml "button1" [putStrLn "Hello, world"]
-- Off we go
mainGUI
This is really the suggestion from augustss' comment. Thoroughly untested, but this will get you started:
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Reader
import Graphics.UI.Gtk
import Graphics.UI.Gtk.Glade
getButton :: String -> ReaderT GladeXML IO Button
getButton buttonName =
do gladeXML <- ask
return . lift $ xmlGetWidget gladeXML castToButton buttonName
To run a ReaderT GladeXML IO
action:
-- Well, you should probably just use `runReaderT` directly, but at least the
-- type signature here is instructive.
runGladeXMLReader :: ReaderT GladeXML IO a -> GladeXML -> IO a
runGladeXMLReader = runReaderT
Try reading the docs on Control.Monad.Trans.Reader
, and some monad transformer tutorials.
Let me try again. What I'm doing is combining two ideas that you can tackle separately, then put back together:
Reader
monadYou might start by reading these to try to understand the Reader
monad:
Reader
("The world of future values")Basically, Reader
is a monad that constructs values that depend on a missing, implicit "environment" value. Within the Reader
monad there is an action called ask :: Reader r r
whose result is the environment value.
So the idea is that everywhere you have GladeXML -> something
, you can rewrite that function into a monadic action of type Reader GladeXML something
. So for example, a simplification of my example above (no monad transformer):
getButton :: String -> Reader GladeXML (IO Button)
getButton buttonName = do
-- The variable gladeXML gets the value of the "implicit" GladeXML value
gladeXML <- ask
-- Now we use that value as an argument to the xmlGetWidget function.
return $ xmlGetWidget gladeXML castToButton buttonName
The way you use a Reader
then is with the runReader :: Reader r a -> r -> a
function. Schematically:
{- NOTE: none of this is guaranteed to even compile... -}
example :: IO Button
example = do
_ <- initGUI -- Setup
Just xml <- loadGladeFile "tutorial.glade"
runReader (getButton "button1") xml
However, since you're using both Reader
and IO
in here, what you want to do is make a combined monad that has the "powers" of both. That's what monad transformers add to the picture. A ReaderT GladeXML IO a
is, conceptually, an IO
action that has access to an "implicit" GladeXML value:
getButton :: String -> ReaderT GladeXML IO Button
getButton buttonName =
do gladeXML <- ask
-- There is one catch: to use any IO action, you have to prefix it with
-- the `lift` function...
button <- lift $ xmlGetWidget gladeXML castToButton buttonName
return button
-- I've refactored this slightly to *not* take a list of actions.
onButtonClick :: String -> ReaderT GladeXML IO a -> ReaderT GladeXML IO ()
onButtonClick gladeXML buttonName action = do
aButton <- getButton buttonName
xml <- ask
_ <- lift $ onClicked aButton (runReaderT action xml)
return ()
-- This is the piece of code that illustrates the payoff of the refactoring.
-- Note how there is no variable being passed around for the xml. This is
-- because I'm making a "big" ReaderT action out of small ones, and they will
-- all implicitly get the same `GladeXML` value threaded through them.
makeButton1 :: ReaderT GladeXML IO Button
makeButton1 =
do button1 <- getButton "button1"
onButtonClick "button1" $ do
lift $ putStrLn "Hello, world"
return button1
-- The `main` action just fetches the `GladeXML` value and hands it off to the
-- actual main logic, which is a `ReaderT` that expects that `GladeXML`
main :: IO ()
main = do
xml <- ...
runReaderT actualMain xml
actualMain :: ReaderT GladeXML IO ()
actualMain = do ...