为什么Reader实现基于ReaderT?

4
1个回答

13

为了尽可能共享代码,在ReaderReaderT之间,它们是相同的数据类型。目前只有runReadermapReaderwithReader有一些特殊情况。而且withReader没有任何独特的代码,它只是一个类型专业化,所以只有两个函数对于ReaderReaderT有一些特殊处理。

你可能会看模块导出并认为这样做并没有什么用,但实际上很有用。有很多ReaderT的实例被定义,并且Reader自动拥有这些实例,因为它是相同的类型。因此,只有一个基础类型为二者提供支持其实需要的代码量大大减少了。

鉴于此,你的问题归结为询问为什么要在ReaderT之上实现Reader,而不是反过来。对于这个问题,嗯,这是唯一可行的方法。

让我们试着走另一条路,看看会出什么问题。

newtype Reader r a = Reader (r -> a)
type ReaderT r m a = Reader r (m a)

没错,好的。内联别名并去掉newtype包装,ReaderT r m a就等同于r -> m a,正如它应该的那样。现在让我们前进到Functor实例:

instance Functor (Reader r) where
    fmap f (Reader g) = Reader (f . g)

是的,根据定义来看,Functor 的唯一可能实例就是这个。由于 ReaderT 是相同的基础类型,它也为 ReaderT 提供了一个 Functor 实例。但是出现了严重的问题。如果你修复第二个参数和结果类型使其符合预期,fmap 就会特化成类型 (m a -> m b) -> ReaderT r m a -> ReaderT r m b ,这完全不正确。 fmap 的第一个参数应该是类型 (a -> b)。两边都有 m 显然是不应该的。

但是,这正是当你试图以 Reader 为基础实现 ReaderT 而不是另一种方式时会发生的事情。为了在这两种类型之间共享 Functor (以及更多) 的代码,每个类型中的最后一个类型变量必须是底层类型中的同一个。而以 Reader 为基础来构建 ReaderT 时这是不可能的。它必须引入一个额外的类型变量,在做所有替换并获得正确结果的情况下,唯一的方法是使 Reader r a 中的 a 指代与 ReaderT r m a 中的不同。结果证明,这与在两种类型之间共享高阶实例(例如 Functor)是不兼容的。

有趣的是,在 Reader 上你选择了最好的情况,因为它有可能让类型正确对齐。如果你尝试以 State 为基础构建 StateT,则会更快地出现问题。例如,甚至无法编写一个类型别名来添加 m 参数并为该对展开正确的内容。在尝试使用 State 为基础时,问题会更快地显现出来。


derivingVia 让编写 newtype Reader e a = Reader e a 并从 ReaderT 中提取实例变得非常容易。当然,这是相当新的。 - dfeuer

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