如何将附加参数传递给 Control.Monad.Reader 实例

How to pass additional arguments to a Control.Monad.Reader instance

我正在阅读 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 文档。

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

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

这种尝试绝对不幼稚,虽然这不是通常的做事方式。

由于 Reader r a 实际上只是幕后的一个函数 r -> a,您对 getArgumentsproduceResponse 的定义本质上是:

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

请注意,在这种情况下,Config 始终是第一个参数,因此 getArguments req 之类的东西将不起作用 - 毕竟,Request 是 [=17= 的第二个参数], 不是第一个,所以你不能只将 getArguments 应用到 req.

您需要做的是先将getArguments应用到Config。由于我们实际上是在使用 Reader Config ... 而不是简单的函数 Config -> ...,因此我们通过绑定 getArguments:

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

由于 getArguments 是一个 Reader Config (Request -> Arguments)argumentGetter 将只是一个函数 Request -> 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 <- getArguments req - getArguments 应用于 req 产生 Reader Config Arguments 并将其绑定到 args 使 args 成为 Arguments
  • produceResponse header args - produceResponse 应用于 headerargs 产生 Reader Config Response,这非常适合作为 [=52= 中的最后一个语句]块

请注意,我们可以对 Reader 进行这样的转换,因为 Reader 只是一个函数,但通常对于任何 Monad m,都有很大的不同在 m (x -> y)x -> m y 之间,后者是“参数化”单子操作最常见的。

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