Monad Transformers vs将参数传递给函数

Bru*_*der 47 haskell monad-transformers

我是Haskell的新手,但了解如何使用Monad变形金刚.然而,我仍然难以获得他们声称的优势,而不是将参数传递给函数调用.

基于wiki Monad Transformers Explained,我们基本上将Config对象定义为

data Config = Config Foo Bar Baz
Run Code Online (Sandbox Code Playgroud)

传递它,而不是用这个签名编写函数

client_func :: Config -> IO ()
Run Code Online (Sandbox Code Playgroud)

我们使用ReaderT Monad Transformer并将签名更改为

client_func :: ReaderT Config IO ()
Run Code Online (Sandbox Code Playgroud)

然后拉动Config只是一个电话ask.

函数调用从更改client_func crunReaderT client_func c

精细.

但为什么这会使我的应用程序变得更简单?

1-我怀疑当你将许多功能/模块拼接在一起形成一个应用程序时,Monad变形金刚会感兴趣.但这就是我的理解停止的地方.有人可以请一些亮点吗?

2-我找不到任何关于如何在Haskell中编写大型模块化应用程序的文档,其中模块公开某种形式的API并隐藏它们的实现,以及(部分地)将其自己的状态和环境隐藏在其他模块中.有什么指针吗?

(编辑:真实世界Haskell声称"......这种方法[Monad变形金刚] ......扩展到更大的程序.",但没有明确的例子证明这种说法)

编辑关注Chris Taylor以下答案

克里斯完美地解释了为什么封装Config,State等等......在Transformer Monad中提供了两个好处:

  1. 它阻止更高级别的函数必须在其类型签名中维护它调用的(子)函数所需的所有参数,但不需要它自己使用(参见getUserInput函数)
  2. 因此,更高级别的功能可以更灵活地改变Transformer Monad的内容(假设您想添加一个Writer以提供更低级别功能的Logging)

这是以更改所有功能的签名为代价的,以便它们在Transformer Monad中"运行".

所以问题1已完全涵盖.谢谢克里斯.

现在在这篇SO帖子中回答了问题2

Chr*_*lor 47

假设我们正在编写一个需要以下形式的配置信息的程序:

data Config = C { logFile :: FileName }
Run Code Online (Sandbox Code Playgroud)

编写程序的一种方法是在函数之间显式传递配置.如果我们只需要将它传递给明确使用它的函数就好了,但遗憾的是我们不确定函数是否需要调用另一个使用该配置的函数,因此我们不得不将其作为一个函数传递给它.参数无处不在(实际上,它往往是需要使用配置的低级函数,这迫使我们将它传递给所有高级函数).

让我们编写这样的程序,然后我们将使用Readermonad 重新编写它,看看我们得到了什么好处.

选项1.显式配置传递

我们最终得到这样的东西:

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
Run Code Online (Sandbox Code Playgroud)

请注意,在高级函数中,我们必须始终传递配置.

选项2.读者monad

另一种方法是使用Readermonad 重写.这有点使低级函数复杂化:

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
Run Code Online (Sandbox Code Playgroud)

但作为我们的奖励,高级功能更简单,因为我们永远不需要参考配置文件.

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

runProgram :: Program ()
runProgram = do input <- getUserInput
                putStrLn $ "You wrote: " ++ input
Run Code Online (Sandbox Code Playgroud)

更进一步

我们可以重写getUserInput和runProgram的类型签名

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

runProgram :: (MonadReader Config m, MonadIO m) => m ()
Run Code Online (Sandbox Code Playgroud)

如果我们决定Program出于任何原因想要更改基础类型,那么这为我们以后提供了很大的灵活性.例如,如果我们想在程序中添加可修改状态,我们可以重新定义

data ProgramState = PS Int Int Int

type Program a = StateT ProgramState (ReaderT Config IO) a
Run Code Online (Sandbox Code Playgroud)

而且我们不需要修改getUserInput或根本没有runProgram- 它们将继续正常工作.

NB我没有打字检查这篇文章,更不用说试图运行它了.可能有错误!