关于 Haskell Monad Transformers 的困惑

4

我对单子变换器中m应该放在哪一侧感到困惑?

例如:

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) }

5
你试过为 m (r -> a) 版本编写单子实例吗?我希望你能将其翻译成中文。 - cdk
2
任何需要读者单子信息(类型变量r)的操作都无法交错类型为m的单子动作,而只能返回类型为a的值。因此,想象一下我是一个带有r ~ FilePath(ReaderT IO FilePath)的读者单子。读者单子可以获取路径,但实际上无法读取文件。 - Thomas M. DuBuisson
1个回答

4
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

如果 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)

但是,对于以不同顺序堆叠 Maybe Reader ,情况则完全不同。 这两种情况同构于“好”阅读器 r -> Maybe a

MaybeT (Reader r)
ReaderT r Maybe

如果我没记错的话,(m a, w) 实际上不是一个有效的 Monad 变换器。 - luqui
@luqui:是的,你说得对。我尝试更新答案以回应你的评论。 - K. A. Buhr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接