Monad Transformers vs Passing parameters to functions

前端 未结 1 684
不思量自难忘°
不思量自难忘° 2021-01-30 01:54

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.

相关标签:
1条回答
  • 2021-01-30 02:13

    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!

    0 讨论(0)
提交回复
热议问题