我对应该将m
放在Monad变压器右侧的位置感到困惑吗?
例如:
WriterT
定义为
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
ReaderT
被定义为
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
但不是
newtype ReaderT r m a = ReaderT { runReaderT :: m (r -> a) }
答案 0 :(得分:4)
monad m
的位置将取决于应用于基础monad m
的monad转换器的功能和操作,因此,它取决于读写器应该具有的功能被添加到monad。
记住runReaderT
和runWriterT
并没有真正做什么,尽管它们具有暗示性的名称,这有助于我们记住。他们只是在包装一个新类型,正是它们包装的东西改变了单子m
。
我的意思是,给定单子m
,您可以通过考虑以下类型的单子动作向其添加阅读器:
r -> m a
,您可以考虑以下类型的单子动作向其中添加编写器:
m (a, w)
,您可以通过考虑以下类型的单子动作向其添加阅读器,作者和状态:
r -> s -> m (a, s, w)
(也就是说,您不需要任何变压器包装器,尽管它们可以使它更加方便,尤其是因为您可以使用>>=
和<*>
之类的现有运算符代替必须自己定义。)
因此,当您将阅读器添加到单子m
时,为什么不将m
放在开头并考虑以下类型的单子动作呢?
m (r -> a)
实际上,您可以这样做,但是您很快就会发现,这种添加阅读器的方法实际上并没有为monad m
添加太多功能。
例如,假设您正在编写一个应在值表中查找键的函数,并且希望将该表携带在读取器中。由于查找可能会失败,因此您想在Maybe
monad中执行此操作。因此,您想要编写如下内容:
myLookup :: Key -> Maybe Value
myLookup key = ...
但是,您想使用提供键和值表的阅读器来增强Maybe
monad。如果我们使用m (r -> a)
模式执行此操作,则会得到:
myLookup :: Key -> Maybe ([(Key,Value)] -> Value)
现在,让我们尝试实现它:
myLookup k = Just (\tbl -> ...)
我们已经看到了一个问题。在允许我们编写代码访问Just
之前,我们必须提供\tbl
(表示查找成功)。也就是说,单子动作(失败或带有返回值的成功)不能依赖于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)
甚至不是单子。它比较弱(只是可应用的)。
请注意,将仅适用的阅读器添加到monad可能仍然有用。它只需要用于计算结构不依赖于r
中信息的计算中即可。 (因此,如果基础monad为Maybe
,以允许计算发出错误信号,则可以将r
中的值用于计算中,但是确定计算是否成功必须独立于r
。)
但是,r -> m a
版本的功能更强大,可以同时用作单子和实用的阅读器。
请注意,某些单子变换可以多种形式使用。例如,您可以(但仅在某些情况下,如@luqui在评论中指出的那样)以两种方式将作家添加到m
monad中:
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) -- acts like (Maybe a, w)
WriterT w Maybe -- acts like Maybe (a, w)
对于Maybe
和Reader
以不同顺序堆叠的情况,不是是相同的。对于“好的”阅读器r -> Maybe a
,这两个都是同构的:
MaybeT (Reader r)
ReaderT r Maybe