这种尝试绝对不幼稚,尽管它不是通常的做事方式。
由于Reader r a实际上只是一个函数r -> a在幕后,你对getArguments和produceResponse的定义本质上是:
getArguments :: Config -> Request -> Arguments
produceResponse :: Config -> Header -> Arguments -> Response
注意在这种情况下Config总是第一个参数,所以getArguments req这样的东西不会起作用——毕竟Request是getArguments的第二个参数,而不是第一个,所以你可以'不只是将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 总是排在第一位。
像这样定义 getArguments 和 produceResponse 在您的原始示例中效果很好:
-
args <- getArguments req - getArguments 应用于 req 产生一个 Reader Config Arguments 并将其绑定到 args 使得 args 一个 Arguments
-
produceResponse header args - produceResponse 应用于 header 和 args 生成 Reader Config Response,它非常适合作为 do 块中的最后一条语句
请注意,我们可以对Reader 进行这样的转换,因为无论如何Reader 只是一个函数,但通常对于任何Monad m,m (x -> y) 和x -> m y 之间存在相当大的差异,后者是“参数化”一元操作中最常见的。
例如,考虑readFile :: FilePath -> IO String。您提供一个FilePath,它会为您提供一个生成文件内容的 IO 操作。相反,如果它是IO (FilePath -> String),它将是一个产生函数FilePath -> String 的IO 操作——也就是说,当给定FilePath 时,它是一个纯函数,它产生一个文件的内容。然而,由于它是一个纯函数,它不会有任何副作用,所以它实际上不能读取文件,因此这不会真正起作用(getFile 可以做一些疯狂的事情,比如读取首先是整个文件系统,但我们先不讨论)。