How can I reduce the number of arguments I have to pass around in Haskell?
Asked Answered
U

1

6

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
Unessential answered 24/6, 2014 at 21:6 Comment(3)
You can stick everything you need in a record and pass that around. Or use a reader monad to avoid even passing that around. – Solano
@Unessential If you do use records you might want to give Jon Sterling's vinyl records a spin (jonmsterling.com/posts/…). I haven't tried them out myself, but I have heard that they solve the namespace issue of records (ghc.haskell.org/trac/ghc/wiki/Records) – Ranking
I'm still trying to wrap my head around the basics. So far my biggest problem is that stuff which has historically been trivial for me to do (using languages like Delphi, C++, Python, Objective-C, Smalltalk, etc) seem incredibly complicated to implement in Haskell. While I am beginning to understand conceptually what's going on with this Reader Monad, it seems like a very complicated way to solve a very simple problem. The machinery needed to get around the "pureness" of functional programming is extraordinarily complex. – Unessential
I
10

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:

  1. The Reader monad
  2. Monad transformers

You might start by reading these to try to understand the Reader monad:

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 ...
Identic answered 24/6, 2014 at 21:35 Comment(8)
If I understood this, I'm sure I'd mark your answer correct immediately. πŸ˜‚ – Unessential
I'm not good enough yet that I can understand these things directly from the docs. I find the docs pretty much require one to already be expert. I'll try to find some tutorials on the Reader. Thanks for taking the time to respond. – Unessential
How does the getButton (say) function get called? My difficulty (and I bet others share this) is that the libraries and docs generally define these things but don't actually show you how to actually use them. That's been my major problem from the beginning. – Unessential
@Unessential I've expanded the answer. See if this helps. I'm afraid I've done everything in a bit of a hurry. – Identic
One could also use a type alias for ReaderT GladeXML IO to make the type signatures more readable. – Candler
Well, although I now understand in principle what's going on, I couldn't get your example working. More importantly, it's not at all clear that the code is simplified by using the ReaderT. The one liner ' onButtonClick xml "button1" [putStrLn "Hello, world"] ' has gotten replaced by stuff that (a) requires much more complexity (b) extra functions (now I need a makeButton1, what about makeButton2, and so forth) and I no longer have an simple sequence of actions. I'm becoming less certain that Haskell is a "better" way to do this kind of thing. – Unessential
The inability to format these comments is extremely frustrating. – Unessential
Generally, one would like to have the main code just be a sequence of that one-liner, i.e, define an event kind, associate it with an "object" and then associate the desired code to be triggered when the event occurs. This seems very awkward to do in Haskell – Unessential

© 2022 - 2024 β€” McMap. All rights reserved.