对Haskell Monad变形金刚感到困惑

时间:2018-08-15 18:13:43

标签: haskell monad-transformers

我对应该将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) }

1 个答案:

答案 0 :(得分:4)

monad m的位置将取决于应用于基础monad m的monad转换器的功能和操作,因此,它取决于读写器应该具有的功能被添加到monad。

记住runReaderTrunWriterT并没有真正做什么,尽管它们具有暗示性的名称,这有助于我们记住。他们只是在包装一个新类型,正是它们包装的东西改变了单子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

如果mIO,那么IO (a,w)(IO a, w)有用得多-后者是书面的w(例如,错误日志)不能取决于执行IO操作的结果!同样,(IO a, w)实际上并不是单子。这只是一个应用。

另一方面,如果mMaybe,那么(Maybe a, w)会写出计算是成功还是失败的信息,而Maybe (a, w)如果返回则丢失所有日志条目Nothing。两种形式都是单子形式,可以在不同情况下使用,它们分别对应于以不同顺序堆叠变压器:

MaybeT (Writer w)  -- acts like  (Maybe a, w)
WriterT w Maybe    -- acts like  Maybe (a, w)

对于MaybeReader以不同顺序堆叠的情况,不是是相同的。对于“好的”阅读器r -> Maybe a,这两个都是同构的:

MaybeT (Reader r)
ReaderT r Maybe