Modular Program Design - Combining Monad Transformers into Monad Agnostic Features

I'm trying to come up with a modular design for the program, and once again, I am kindly asking for your help.

As a continuation of the following posts by Monad Transformers vs pass Parameters and Scale Design in Haskell I am trying to create two independent modules that use Monad Transformers but expose Monad-agnostic functions, and then combine the Monad-agnostic function from each of these modules into a new Monad-agnostic function.

I was unable to start the union function, for example. how can i call mainProgram using runReaderT in the example below ?.

Child question: is there a better way to achieve the same modular design goal?




In the example, there are two mock (but compilation) modules, one of which performs registration, and one that reads the user input and manipulates it. The merge function reads user input, registers it, and prints.

 {-# LANGUAGE FlexibleContexts #-} module Stackoverflow2 where import Control.Monad.Reader ---- ---- From Log Module - Writes the passed message in the log ---- data LogConfig = LC { logFile :: FilePath } doLog :: (MonadIO m, MonadReader LogConfig m) => String -> m () doLog _ = undefined ---- ---- From UserProcessing Module - Reads the user Input and changes it to the configured case ---- data MessageCase = LowerCase | UpperCase deriving (Show, Read) getUserInput :: (MonadReader MessageCase m, MonadIO m) => m String getUserInput = undefined ---- ---- Main program that combines the two ---- mainProgram :: (MonadReader MessageCase m, MonadReader LogConfig m, MonadIO m) => m () mainProgram = do input <- getUserInput doLog input liftIO $ putStrLn $ "Entry logged: " ++ input 
+13
haskell monad-transformers
Oct 22 '12 at 7:55
source share
2 answers

Your mainProgram signature mainProgram problematic because the MonadReader class contains a functional dependency MonadReader rm | m -> r MonadReader rm | m -> r . This essentially means that one particular type cannot have an instance of MonadReader for several different types. Therefore, when you say that type m has both MonadReader MessageCase and MonadReader LogConfig , it goes against declaring the dependency.

The simplest solution is to change mainProgram to a non-generic type:

 mainProgram :: ReaderT MessageCase (ReaderT LogConfig IO) () mainProgram = do input <- getUserInput lift $ doLog input liftIO $ putStrLn $ "Entry logged: " ++ input 

It also requires an explicit lift for doLog .

Now you can run mainProgram by running each ReaderT separately, for example:

 main :: IO () main = do let messageCase = undefined :: MessageCase logConfig = undefined :: LogConfig runReaderT (runReaderT mainProgram messageCase) logConfig 

If you want to have a common function that uses two different instances of MonadReader , you need to make it explicit in the signature that one reader is a monad transformer on top of another reader.

 mainProgram :: (MonadTrans mt, MonadReader MessageCase (mt m), MonadReader LogConfig m, MonadIO (mt m), MonadIO m) => mt m () mainProgram = do input <- getUserInput lift $ doLog input liftIO $ putStrLn $ "Entry logged: " ++ input 

However, this has the unfortunate effect that the function is no longer completely general, because the order in which two readers appear on the monad stack is blocked. There may be a cleaner way to achieve this, but I could not figure it out of my head without sacrificing (even more) the pedigree.

+10
Oct 22
source share

There is a way to write a fully modular version of the program. The way you need to approach the problem is to combine your reader’s configuration into a single data structure, and then define the type classes that describe the partial interface that specific functions need for that data structure. For example:

 class LogConfiguration c where logFile :: c -> FilePath doLog :: (MonadIO m, LogConfiguration c, MonadReader cm) => String -> m () doLog = do file <- asks logFile -- ... class MessageCaseConfiguration c where isLowerCase :: c -> Bool getUserInput :: (MonadIO m, MessageCaseConfiguration c, MonadReader cm) => m String getUserInput = do lc <- asks isLowerCase -- ... data LogConfig = LC { logConfigFile :: FilePath } data MessageCase = LowerCase | UpperCase data Configuration = Configuration { logging :: LogConfig, casing :: MessageCase } instance LogConfiguration Configuration where logFile = logConfigFile . logging instance MessageCaseConfiguration Configuration where isLowerCase c = case casing c of LowerCase -> True UpperCase -> False mainProgram :: (MonadIO m, MessageCaseConfiguration c, LogConfiguration c, MonadReader cm) => m () mainProgram = do input <- getUserInput doLog input liftIO . putStrLn $ "Entry logged: " ++ input 

Now you can call mainProgram with Configuration in the ReaderT monad, and it will work as you expected.

+20
Oct 22
source share



All Articles