m a -> ReaderT r m a
Similarly, you can add a writer to m
by considering monadic actions of type:
m a -> WriterT w m a
The resulting monad transformer will transform the behavior of m
by adding either reader or writer functionality, depending on which transformer is used.
r -> m a
您可以通过考虑类型为单子动作的方式向其添加写入器:
m (a, w)
通过考虑类型为 monadic actions 的读写器和状态,您可以将其添加到其中:
r -> s -> m (a, s, w)
(也就是说,你不需要任何变换器包装器来完成这个操作,尽管它们可以让它更方便,特别是因为你可以使用现有的运算符如
>>=
和
<*>
而不必定义自己的运算符。)
所以,当你向单子m添加一个读取器时,为什么不在开头放置
m
并考虑以下类型的单子动作呢?
m (r -> a)
实际上,你确实可以这样做,但很快就会发现这种添加读取器的方法实际上并没有向单子 m
添加太多功能。
例如,假设你正在编写一个函数,该函数应在值表中查找键,并希望在 reader 中携带该表。由于查找可能失败,因此您希望在 Maybe
单子中执行此操作。因此,您需要编写类似于以下内容的代码:
myLookup :: Key -> Maybe Value
myLookup key = ...
然而,你想要用一个提供键和值表的 reader 来增强 Maybe
monad。如果我们使用 m (r -> a)
模式来实现这个目标,我们可以得到:
myLookup :: Key -> Maybe ([(Key,Value)] -> Value)
现在,让我们尝试实现它:
myLookup k = Just (\tbl -> ...)
我们已经发现了一个问题。在访问 \tbl
的代码之前,我们必须提供一个 Just
(表示查找成功)。也就是说,单子动作(失败或成功并带有返回值)不能依赖于 r
中应该从签名 m (r -> a)
明显的信息。使用交替的 r -> m a
模式更为强大:
type M a = ([Key,Value]) -> Maybe a
myLookup :: Key -> M Value
myLookup key tbl = Prelude.lookup key tbl
@Thomas_M_DuBuisson举了另一个例子。如果我们想读取一个输入文件,我们可能会写成:
readInput :: FilePath -> IO DataToProcess
readInput fp = withFile fp ReadMode $ \h -> ...
带着配置信息(例如文件路径)在不同的读取器之间移植会非常方便,因此让我们使用模式m (r -> a)
来转换它:
data Config = Config { inputFile :: FilePath }
readConfig :: IO (Config -> DataToProcess)
readConfig = ...um...
我们陷入了困境,因为我们无法编写依赖于配置信息的IO操作。 如果我们使用备选模式r -> m a
,那么我们就可以解决问题:
type M a = Config -> IO a
readConfig :: M DataToProcess
readConfig cfg = withFile (inputFile cfg) ReadMode $ ...
另一个问题,由@cdk提出,是这种新的“单子”操作类型:
m (r -> a)
它甚至不是一个单子,只是一个较弱的应用子。
请注意,在单子中添加仅仅是应用子的读取器仍然可能很有用。它只需要在计算结构不依赖于 r
中的信息的计算中使用。(因此,如果基础单子是 Maybe
,以允许计算发出错误信号,则可以在计算中使用来自 r
的值,但是计算成功与否的确定必须独立于 r
。)
然而,r -> m a
版本更加强大,并且可以用作单子和应用子的读取器。
请注意,某些单子变换在多种形式中都很有用。例如,您可以(但只有在某些情况下,如 @luqui 在评论中指出的那样)以两种方式将写入器添加到 m
单子中:
m (a, w) -- if m is a monad this is always a monad
(m a, w) -- this is a monad for some, but not all, monads m
如果
m
是
IO
,那么
IO (a,w)
比
(IO a, w)
更加有用——后者中写入的
w
(例如错误日志)不能依赖于执行
IO
操作的结果!另外,再次强调,
(IO a, w)
实际上不是一个单子;它只是一个应用子。
另一方面,如果
m
是
Maybe
,那么
(Maybe a, w)
会在计算成功或失败时都写入一些内容,而
Maybe (a, w)
如果返回
Nothing
就会失去所有日志条目。这两种形式都是单子,可以在不同情况下发挥作用,并且它们对应于以不同顺序堆叠变换器。
MaybeT (Writer w)
WriterT w Maybe
但是,对于以不同顺序堆叠 Maybe
和 Reader
,情况则完全不同。 这两种情况同构于“好”阅读器 r -> Maybe a
:
MaybeT (Reader r)
ReaderT r Maybe
m (r -> a)
版本编写单子实例吗?我希望你能将其翻译成中文。 - cdkr
)的操作都无法交错类型为m
的单子动作,而只能返回类型为a
的值。因此,想象一下我是一个带有r ~ FilePath
(ReaderT IO FilePath)的读者单子。读者单子可以获取路径,但实际上无法读取文件。 - Thomas M. DuBuisson