Reader
是基于ReaderT
使用Identity
实现的。为什么不先制作Reader
,然后再制作ReaderT
呢?是否有特定的原因以这种方式实现?你可以在此处查看更多信息:https://hackage.haskell.org/package/transformers-0.6.0.2/docs/src/Control.Monad.Trans.Reader.html#ReaderT
Reader
是基于ReaderT
使用Identity
实现的。为什么不先制作Reader
,然后再制作ReaderT
呢?是否有特定的原因以这种方式实现?你可以在此处查看更多信息:https://hackage.haskell.org/package/transformers-0.6.0.2/docs/src/Control.Monad.Trans.Reader.html#ReaderT
为了尽可能共享代码,在Reader
和ReaderT
之间,它们是相同的数据类型。目前只有runReader
、mapReader
和withReader
有一些特殊情况。而且withReader
没有任何独特的代码,它只是一个类型专业化,所以只有两个函数对于Reader
与ReaderT
有一些特殊处理。
你可能会看模块导出并认为这样做并没有什么用,但实际上很有用。有很多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