53

I am new to Haskell but understand how Monad Transformers can be used. Yet, I still have difficulties grabbing their claimed advantage over passing parameters to function calls.

Based on the wiki Monad Transformers Explained, we basically have a Config Object defined as

data Config = Config Foo Bar Baz

and to pass it around, instead of writing functions with this signature

client_func :: Config -> IO ()

we use a ReaderT Monad Transformer and change the signature to

client_func :: ReaderT Config IO ()

pulling the Config is then just a call to ask.

The function call changes from client_func c to runReaderT client_func c

Fine.

But why does this make my application simpler ?

1- I suspect Monad Transformers have an interest when you stitch a lot of functions/modules together to form an application. But this is where is my understanding stops. Could someone please shed some light?

2- I could not find any documentation on how you write a large modular application in Haskell, where modules expose some form of API and hide their implementations, as well as (partly) hide their own States and Environments from the other modules. Any pointers please ?

(Edit: Real World Haskell states that ".. this approach [Monad Transformers] ... scales to bigger programs.", but there is no clear example demonstrating that claim)

EDIT Following Chris Taylor Answer Below

Chris perfectly explains why encapsulating Config, State,etc... in a Transformer Monad provides two benefits:

  1. It prevents a higher level function from having to maintain in its type signature all the parameters required by the (sub)functions it calls but not required for its own use (see the getUserInput function)
  2. and as a consequence makes higher level functions more resilient to a change of the content of the Transformer Monad (say you want to add a Writer to it to provide Logging in a lower level function)

This comes at the cost of changing the signature of all functions so that they run "in" the Transformer Monad.

So question 1 is fully covered. Thank you Chris.

Question 2 is now answered in this SO post

Community
  • 1
  • 1
Bruno Grieder
  • 28,128
  • 8
  • 69
  • 101
  • 4
    Transformers are also useful when you want the features of some monad temporarily, for a sub-computation. For example, I’ve used `WriterT` to log information while updating a data structure with unique identifiers generated using `State`. – Jon Purdy Oct 19 '12 at 08:18

1 Answers1

51

Let's say that we're writing a program that needs some configuration information in the following form:

data Config = C { logFile :: FileName }

One way to write the program is to explicitly pass the configuration around between functions. It would be nice if we only had to pass it to the functions that use it explicitly, but sadly we're not sure if a function might need to call another function that uses the configuration, so we're forced to pass it as a parameter everywhere (indeed, it tends to be the low-level functions that need to use the configuration, which forces us to pass it to all the high-level functions as well).

Let's write the program like that, and then we'll re-write it using the Reader monad and see what benefit we get.

Option 1. Explicit configuration passing

We end up with something like this:

readLog :: Config -> IO String
readLog (C logFile) = readFile logFile

writeLog :: Config -> String -> IO ()
writeLog (C logFile) message = do x <- readFile logFile
                                  writeFile logFile $ x ++ message

getUserInput :: Config -> IO String
getUserInput config = do input <- getLine
                         writeLog config $ "Input: " ++ input
                         return input

runProgram :: Config -> IO ()
runProgram config = do input <- getUserInput config
                       putStrLn $ "You wrote: " ++ input

Notice that in the high level functions we have to pass config around all the time.

Option 2. Reader monad

An alternative is to rewrite using the Reader monad. This complicates the low level functions a bit:

type Program = ReaderT Config IO

readLog :: Program String
readLog = do C logFile <- ask
             readFile logFile

writeLog :: String -> Program ()
writeLog message = do C logFile <- ask
                      x <- readFile logFile
                      writeFile logFile $ x ++ message

But as our reward, the high level functions are simpler, because we never need to refer to the configuration file.

getUserInput :: Program String
getUserInput = do input <- getLine
                  writeLog $ "Input: " ++ input
                  return input

runProgram :: Program ()
runProgram = do input <- getUserInput
                putStrLn $ "You wrote: " ++ input

Taking it further

We could re-write the type signatures of getUserInput and runProgram to be

getUserInput :: (MonadReader Config m, MonadIO m) => m String

runProgram :: (MonadReader Config m, MonadIO m) => m ()

which gives us a lot of flexibility for later, if we decide that we want to change the underlying Program type for any reason. For example, if we want to add modifiable state to our program we could redefine

data ProgramState = PS Int Int Int

type Program a = StateT ProgramState (ReaderT Config IO) a

and we don't have to modify getUserInput or runProgram at all - they'll continue to work fine.

N.B. I haven't type checked this post, let alone tried to run it. There may be errors!

Chris Taylor
  • 46,912
  • 15
  • 110
  • 154
  • Thank you Chris. It makes perfect sense and perfectly describes the 'hiding effect' benefit. It also raises a couple of additional questions on the rules functions signatures in a large App should obey as well as the general structure of the App. I propose to let the question run for a few hours to see if we get more input. I will then come back with a better formalization of any remaining request for clarification. – Bruno Grieder Oct 19 '12 at 08:28
  • Chris. I am testing your example to try generalizing it (the "taking it further" part). My problem is with the signature `getUserInput :: (MonadReader m, MonadIO m) => m String`. As is, the line `writeLog $ "Input: " ++ input` will fail compilation with `Could not deduce (m ~ ReaderT Config IO)`. I can see why... but how do you fix it? – Bruno Grieder Oct 19 '12 at 15:15
  • 1
    It should be `getUserInput :: (MonadReader Config m, MonadIO m) => m String` (and you'll need `FlexibleContexts` enabled). Also, all of the functions called by the generalized function need the generalized type too. The generalized type is bigger than the specific type, and you can't fit a big type into a small type (but you can fit a small type into a big type at the top, when you call `runProgram` with a specific `Monad`). – pat Oct 20 '12 at 06:55
  • @pat - Many Thanks - I was able to completely rewrite Chris example with your help. I am now trying to get it one step further (my question 2) but unfortunately hit a wall again. My woes are described [here](http://stackoverflow.com/questions/13007123/modular-program-design-combining-monad-transformers-into-monad-agnostic-functi) – Bruno Grieder Oct 22 '12 at 07:58
  • For some unfathomable reason a totally valid edit to this post was rejected (by a moderator!). I have edited the post myself and thank the edit suggester for catching my error. – Chris Taylor Jan 12 '15 at 15:01
  • @ChrisTaylor: Can you recommend the right way to learn monadic transformers from the very beginner to advanced ? What is the right way to start studying monads ? – alinsoar Aug 18 '16 at 16:15
  • 1
    @alinsoar I think the best monad tutorial is "You could have invented monads (and maybe you already have)" which is found [here](http://blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html). The same author has a post on [monad transformers](http://blog.sigfpe.com/2006/05/grok-haskell-monad-transformers.html) as well, which I seem to remember was pretty good. – Chris Taylor Aug 18 '16 at 17:12
  • @ChrisTaylor: Thanks! I also have a difficulty reading the arrow schemes. Can you recommend me a way how to learn how to interpret the arrows and transform arrow schemes in program code ? I would like to find something practical, because if I experiment in the same time as reading it's the best way to uinderstand. – alinsoar Aug 18 '16 at 20:56
  • What do you mean by "the arrow schemes"? Can you give an example of something you don't understand? – Chris Taylor Aug 19 '16 at 07:58
  • yes. I would like to learn in a practical way to program with such things: http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf ; http://www.cse.chalmers.se/~rjmh/Papers/arrows.pdf ; https://en.wikibooks.org/wiki/Haskell/Understanding_arrows – alinsoar Aug 19 '16 at 12:50
  • I want to find the good way how to start learning from scratch arrows (in particular monads) and in the same time what I am reading to show me concrete examples that I can experiment – alinsoar Aug 19 '16 at 12:51
  • I see. To be honest, I wouldn't bother learning about them. Certainly not if you don't already understand monads and monad transformers. Arrows are much less useful than they initially appear. If you really want to learn about them, the [Typeclassopedia](https://wiki.haskell.org/Typeclassopedia#Arrow) is a good place to start. – Chris Taylor Aug 19 '16 at 12:53
  • Thanks! I did not know arrows are not too useful. I have read the article `You Could Have Invented Monads!`. It is good, but it's not enough to make me think directly in monadic way of programming. I want something written in the style of this article, but much more elaborated, to have me write a lot of examples and when I finish to be able to think monads in a natural way, how to write recursion and any other problem using monads... – alinsoar Aug 19 '16 at 12:58
  • @ChrisTaylor: and one more question, as I see you master the monads... Can you recommend me other language apart from haskell that allows me to experiment with monads and understand them ? Monads can be implemented in all languages, but I suspect not all languages allows you to see directly inside monads... – alinsoar Aug 19 '16 at 15:43
  • I took the beginner course of haskell programming with Erik Meijer, but clearly it was not enough for me to understand the topic... I do not have much experience, only the materials from that course... – alinsoar Aug 19 '16 at 15:44
  • 1
    @alinsoar Scala has monads (called for comprehensions) and you can use them in OCaml as well (with a module instead of a type class) although I believe the syntax is not as nice. Python has list comprehensions and generator comprehensions, which you could think of as an implementation of the list monad (although it allows you to do IO as well). As you said, you can implement monads in any language, but without type classes, parametric polymorphism and built-in syntax for them, it is a bit clunky. – Chris Taylor Aug 21 '16 at 17:56
  • 1
    @alinsoar The only way to get more experience is to get more experience. There's no magic route to understanding any programming language. Just use the language to solve problems, read books, articles and papers about it, and ask/answer questions on Stack Overflow. Understanding will come. – Chris Taylor Aug 21 '16 at 17:58