53

我是 Haskell 的新手,但了解如何使用 Monad Transformers。然而,我仍然难以抓住他们声称的将参数传递给函数调用的优势。

基于wiki Monad Transformers Explained,我们基本上有一个配置对象定义为

data Config = Config Foo Bar Baz

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

client_func :: Config -> IO ()

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

client_func :: ReaderT Config IO ()

拉动 Config 只是对ask.

函数调用从client_func c变为runReaderT client_func c

美好的。

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

1-当您将许多功能/模块拼接在一起以形成应用程序时,我怀疑 Monad Transformers 有兴趣。但这就是我的理解停止的地方。有人可以阐明一下吗?

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

(编辑:真实世界的 Haskell 声明“.. 这种方法 [Monad Transformers] ......适用于更大的程序。”,但没有明确的例子证明这种说法)

编辑以下克里斯泰勒的回答

Chris 完美地解释了为什么在 Transformer Monad 中封装 Config、State 等会带来两个好处:

  1. 它防止更高级别的函数必须在其类型签名中维护它调用的(子)函数所需的所有参数,但它自己使用不需要(参见getUserInput函数)
  2. 因此,使更高级别的函数对 Transformer Monad 内容的更改更具弹性(例如,您想在其中添加一个Writer以在较低级别的函数中提供 Logging)

这是以更改所有函数的签名为代价的,以便它们“在”Transformer Monad 中运行。

因此,问题 1 已完全涵盖。谢谢克里斯。

问题 2 现在已在此 SO 帖子中得到解答

4

1 回答 1

51

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

data Config = C { logFile :: FileName }

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

让我们像这样编写程序,然后我们将使用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

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

选项 2. Reader 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

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

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

runProgram :: Program ()
runProgram = do input <- getUserInput
                putStrLn $ "You wrote: " ++ input

更进一步

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

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

runProgram :: (MonadReader Config m, MonadIO m) => m ()

如果我们决定Program出于任何原因更改底层类型,这给了我们很大的灵活性。例如,如果我们想为我们的程序添加可修改的状态,我们可以重新定义

data ProgramState = PS Int Int Int

type Program a = StateT ProgramState (ReaderT Config IO) a

我们不必修改getUserInput或根本不需要修改runProgram——它们会继续正常工作。

注意我没有检查过这篇文章,更不用说尝试运行它了。可能有错误!

于 2012-10-19T08:09:34.577 回答