Why Reader implemented based ReaderT?
They are the same data type to share as much code as possible between Reader
and ReaderT
. As it stands, only runReader
, mapReader
, and withReader
have any special cases. And withReader
doesn't have any unique code, it's just a type specialization, so only two functions actually do anything special for Reader
as opposed to ReaderT
.
You might look at the module exports and think that isn't buying much, but it actually is. There are a lot of instances defined for ReaderT
that Reader
automatically has as well, because it's the same type. So it's actually a fair bit less code to have only one underlying type for the two.
Given that, your question boils down to asking why Reader
is implemented on top of ReaderT
, and not the other way around. And for that, well, it's just the only way that works.
Let's try to go the other direction and see what goes wrong.
newtype Reader r a = Reader (r -> a)
type ReaderT r m a = Reader r (m a)
Yep, ok. Inline the alias and strip out the newtype wrapping and ReaderT r m a
is equivalent to r -> m a
, as it should be. Now let's move forward to the Functor
instance:
instance Functor (Reader r) where
fmap f (Reader g) = Reader (f . g)
Yep, it's the only possible instance for Functor
for that definition of Reader
. And since ReaderT
is the same underlying type, it also provides an instance of Functor
for ReaderT
. Except something has gone horribly wrong. If you fix the second argument and result types to be what you'd expect, fmap
specializes to the type (m a -> m b) -> ReaderT r m a -> ReaderT r m b
. That's not right at all. fmap
's first argument should have the type (a -> b)
. That m
on both sides is definitely not supposed to be there.
But it's just what happens when you try to implement ReaderT
in terms of Reader
, instead of the other way around. In order to share code for Functor
(and a lot more) between the two types, the last type variable in each has to be the same thing in the underlying type. And that's just not possible when basing ReaderT
on Reader
. It has to introduce an extra type variable, and the only way to do it while getting the right result from doing all the substitutions is by making the a
in Reader r a
refer to something different than the a
in ReaderT r m a
. And that turns out to be incompatible with sharing higher-kinded instances like Functor
between the two types.
Amusingly, you sort of picked the best possible case with Reader
in that it's possible to get the types to line up right at all. Things fail a lot faster if you try to base StateT
on State
, for instance. There's no way to even write a type alias that will add the m
parameter and expand to the right thing for that pair. Reader
requires you to explore further before things break down.