15

我正在尝试提出模块化程序设计,我再次请求您的帮助。

作为这些后续帖子Monad Transformers vs Passing Parameters and Large Scale Design in Haskell的后续,我正在尝试构建两个独立的模块,它们使用 Monad Transformers 但公开与 Monad 无关的函数,然后结合每个模块中的 Monad 不可知函数这些模块成为一个新的与 Monad 无关的函数。

我一直无法运行组合功能,例如如何在下面的示例中调用mainProgramusing runReaderT

次要问题是:是否有更好的方法来实现相同的模块化设计目标?


该示例有两个模拟模块(但可以编译),一个执行日志记录,一个读取用户输入并对其进行操作。组合功能读取用户输入,记录并打印。

{-# 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
4

2 回答 2

22

有一种方法可以编写程序的完全模块化版本。解决问题所需的方法是将阅读器配置捆绑到一个数据结构中,然后定义类型类来描述特定功能对该数据结构所需的部分接口。例如:

class LogConfiguration c where
  logFile :: c -> FilePath

doLog :: (MonadIO m, LogConfiguration c, MonadReader c m) => String -> m ()
doLog = do
  file <- asks logFile
  -- ...

class MessageCaseConfiguration c where
  isLowerCase :: c -> Bool

getUserInput :: (MonadIO m, MessageCaseConfiguration c, MonadReader c m) => 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 c m) => m ()
mainProgram = do
  input <- getUserInput
  doLog input
  liftIO . putStrLn $ "Entry logged: " ++ input

现在你可以在monad 中调用mainPrograma ,它会像你期望的那样工作。ConfigurationReaderT

于 2012-10-22T11:06:10.280 回答
11

您的mainProgram签名有问题,因为MonadReader类型类包含功能依赖MonadReader r m | m -> r。这实质上意味着单个具体类型不能有MonadReader多个不同类型的实例。因此,当您说该类型m具有两个实例MonadReader MessageCase并且MonadReader LogConfig它违反依赖声明时。

最简单的解决方案是更改mainProgram为具有非泛型类型:

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

这也需要明确lift的 for doLog

现在您可以mainProgram通过单独运行每个来运行ReaderT,如下所示:

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

如果您想要一个使用两个不同MonadReader实例的通用函数,您需要在签名中明确说明一个阅读器是另一个阅读器之上的单子转换器。

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

然而,这有一个不幸的结果是函数不再是完全通用的,因为两个读取器出现在 monad 堆栈中的顺序是锁定的。也许有一种更简洁的方法可以实现这一点,但我无法在不牺牲(甚至更多)通用性的情况下从头顶想出一个。

于 2012-10-22T09:38:48.517 回答