【问题标题】:Monad Transformers vs Passing parameters to functionsMonad Transformers vs 将参数传递给函数
【发布时间】:2012-10-19 06:13:23
【问题描述】:

我是 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 ()

然后拉动配置只是调用ask

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

很好。

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

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

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

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

在下面的 Chris Taylor 回答之后编辑

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

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

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

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

问题 2 现已在this SO post得到解答

【问题讨论】:

  • 当您暂时需要某些 monad 的功能时,Transformers 也很有用,用于子计算。例如,我使用WriterT 记录信息,同时使用State 生成的唯一标识符更新数据结构。

标签: haskell monad-transformers


【解决方案1】:

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

data Config = C { logFile :: FileName }

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

让我们这样编写程序,然后我们将使用Reader monad 重新编写它,看看我们会得到什么好处。

选项 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

另一种方法是使用Reader monad 重写。这使底层函数有点复杂:

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

我们根本不需要修改 getUserInputrunProgram - 它们会继续正常工作。

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

【讨论】:

  • 谢谢克里斯。它非常有意义,完美地描述了“隐藏效应”的好处。它还对大型应用程序中的函数签名应遵守的规则以及应用程序的一般结构提出了几个额外的问题。我建议让这个问题运行几个小时,看看我们是否能得到更多的意见。然后,我将带着更好的形式化任何剩余的澄清请求回来。
  • 应该是getUserInput :: (MonadReader Config m, MonadIO m) =&gt; m String(你需要启用FlexibleContexts)。此外,广义函数调用的所有函数也需要广义类型。广义类型大于特定类型,您不能将大类型放入小类型中(但您可以将小类型放入顶部的大类型中,当您使用特定的 runProgram 调用 @987654337 时@)。
  • @alinsoar 我认为最好的 monad 教程是“你本可以发明 monads(也许你已经发明了)”,它位于here。同一位作者在monad transformers 也有一个帖子,我记得好像还不错。
  • @alinsoar Scala 有 monad(需要理解),您也可以在 OCaml 中使用它们(使用模块而不是类型类),尽管我认为语法不是很好。 Python 具有列表推导和生成器推导,您可以将其视为列表 monad 的实现(尽管它也允许您执行 IO)。正如你所说,你可以用任何语言实现 monad,但是没有类型类、参数多态性和内置语法,它有点笨拙。
  • @alinsoar 获得更多经验的唯一方法就是获得更多经验。理解任何编程语言都没有神奇的途径。只需使用该语言解决问题,阅读有关它的书籍、文章和论文,并在 Stack Overflow 上提问/回答问题。理解就会到来。
猜你喜欢
  • 2013-01-27
  • 2021-04-18
  • 1970-01-01
  • 1970-01-01
  • 2014-02-07
相关资源
最近更新 更多