【问题标题】:How to pass additional arguments to a Control.Monad.Reader instance如何将附加参数传递给 Control.Monad.Reader 实例
【发布时间】:2021-12-20 13:57:15
【问题描述】:

我正在阅读 Book of Monads 并进入 Reader monad,其中使用此示例展示了使用 Reader 的动机:

handle :: Config -> Request -> Response
handle cfg req =
    produceResponse cfg (initializeHeader cfg) (getArguments cfg req)

为避免显式传递cfg 参数,Reader 使用如下:

handle :: Request -> Reader Config Response
handle req = do header <- initializeHeader
                args <- getArguments req
                produceResponse header args

让我感到困惑的部分是cfg 旁边的函数带有附加参数。我怎样才能做到这一点?

在一个非常天真的尝试中,我尝试了这个:

getArguments :: Reader Config (Request -> Arguments)
produceResponse :: Reader Config (Header -> Arguments -> Response)

我还在几本书和Reader 文档中进行了搜索。

【问题讨论】:

  • 你问的是getArgumentsproduceResponse的类型吗?
  • do 块中这两个函数的类型和对应的调用。
  • 你的getArguments 应该有Request -&gt; Reader Config Arguments 类型,确定吗? (produceResponse 也是如此。)

标签: haskell functional-programming


【解决方案1】:

我过去使用的一个选项是创建自定义记录类型

data Context = Context
   { config :: Config
   , header :: Header
   , args :: Arguments
   }

然后,你可以这样使用它:

foo :: Int -> Reader Context String
foo x = do
   h <- asks header        -- get the header from the implicit context
   a <- asks args
   doSomething x h a

【讨论】:

  • 这真的回答了这个问题吗?我想可以定义一个GetArgumentsContext,然后执行args &lt;- withReader (\cfg -&gt; GetArgumentsContext { cfg = cfg, header = header }) getArguments 之类的操作,虽然在某些情况下这样的转换肯定会有价值,但我认为情况并非如此。
  • @f4st 我无法完全理解 OP 真正需要什么。如果他们需要传递多个值,这是一种方法,只要从一开始就知道所有这些值。相反,如果有时你想传递 X 和 Y,有时你想传递 Y 和 Z,等等,那么使用这样的大记录是不可行的,或者至少是不方便的。
  • 我的理解是OP希望示例代码(第二个实际使用Reader)按原样工作。也就是说,他们只想传递Config,而不是其他任何东西。但是,是的,当有更多的东西要传递时,你的解决方案就可以工作,而这不仅仅是一次性的。
【解决方案2】:

这种尝试绝对不幼稚,尽管它不是通常的做事方式。

由于Reader r a实际上只是一个函数r -&gt; a在幕后,你对getArgumentsproduceResponse的定义本质上是:

getArguments :: Config -> Request -> Arguments
produceResponse :: Config -> Header -> Arguments -> Response

注意在这种情况下Config总是第一个参数,所以getArguments req这样的东西不会起作用——毕竟RequestgetArguments的第二个参数,而不是第一个,所以你可以'不只是将getArguments 应用于req

您需要做的是首先将getArguments 应用到Config。因为我们实际上是在使用Reader Config ... 而不是一个简单的函数Config -&gt; ...,所以我们通过绑定getArguments 来做到这一点:

handle req = do header <- initializeHeader
                argumentGetter <- getArguments
                -- rest omitted

由于getArgumentsReader Config (Request -&gt; Arguments)argumentGetter 将只是一个函数Request -&gt; Arguments。使用这样的函数就很简单了:

handle :: Request -> Reader Config Response
handle req = do header <- initializeHeader
                argumentGetter <- getArguments
                let args = argumentGetter req
                -- rest omitted

然后您可以继续对produceResponse 应用相同的处理方法,一切都会奏效。

然而,一开始我说这不是通常的做事方式。考虑以下定义:

getArguments :: Request -> Reader Config Arguments
produceResponse :: Header -> Arguments -> Reader Config Response

如果我们打开 Reader,我们会发现它们实际上只是:

getArguments :: Request -> Config -> Arguments
produceResponse :: Header -> Arguments -> Config -> Response

也就是说,Config 总是作为最后一个参数出现。这与我们一开始的约定是一致的,Config 总是排在第一位。

像这样定义 getArgumentsproduceResponse 在您的原始示例中效果很好:

  • args &lt;- getArguments req - getArguments 应用于 req 产生一个 Reader Config Arguments 并将其绑定到 args 使得 args 一个 Arguments
  • produceResponse header args - produceResponse 应用于 headerargs 生成 Reader Config Response,它非常适合作为 do 块中的最后一条语句

请注意,我们可以对Reader 进行这样的转换,因为无论如何Reader 只是一个函数,但通常对于任何Monad mm (x -&gt; y)x -&gt; m y 之间存在相当大的差异,后者是“参数化”一元操作中最常见的。

例如,考虑readFile :: FilePath -&gt; IO String。您提供一个FilePath,它会为您提供一个生成文件内容的 IO 操作。相反,如果它是IO (FilePath -&gt; String),它将是一个产生函数FilePath -&gt; String 的IO 操作——也就是说,当给定FilePath 时,它是一个纯函数,它产生一个文件的内容。然而,由于它是一个纯函数,它不会有任何副作用,所以它实际上不能读取文件,因此这不会真正起作用(getFile 可以做一些疯狂的事情,比如读取首先是整个文件系统,但我们先不讨论)。

【讨论】:

猜你喜欢
  • 2015-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-21
  • 2013-01-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多