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:
getUserInput
function)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
Monad transformers not only make it easier to write getPassphrase but also simplify all the code instances.
What is a Monad? A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.
A monadic function is a function with a single argument, written to its right. It is one of three possible function valences; the other two are dyadic and niladic. The term prefix function is used outside of APL to describe APL's monadic function syntax.
In Haskell a monad is represented as a type constructor (call it m ), a function that builds values of that type ( a -> m a ), and a function that combines values of that type with computations that produce values of that type to produce a new computation for values of that type ( m a -> (a -> m b) -> m b ).
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.
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.
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
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With