0

I've written the following monad transformers (which I believe are equivalent to each other), namely:

newtype MonadReaderT1 r m a = MonadReaderT (ReaderT (r m) m a)
newtype MonadReaderT2 r m a = MonadReaderT (ReaderT (m r) m a)

The purpose of these is that I basically want a ReaderT, but my environment has to be accessed inside the Monad, it's not actually a fixed pure value (in my case, it's an auth token that needs to be periodically refreshed).

The reason why I think MonadReaderT1 and MonadReaderT2 are equivalent, because I can just go:

newtype B m = B (m A)

And then MonadReaderT1 B is the same as MonadReaderT2 A.

But I think I need this extra machinery here above and beyond what I get with plain old ReaderT.

But I get the feeling I'm not the first person to have done this or needed this. Have I just reinvented an existing type, and if so what is it?

Clinton
  • 22,361
  • 15
  • 67
  • 163
  • 3
    Including monadic functions or actions in a reader environment is a common pattern in Haskell, sometimes called the "ReaderT pattern". https://www.fpcomplete.com/blog/2017/06/readert-design-pattern/ It's a form of dependency injection, and there are libraries to support it like RIO https://hackage.haskell.org/package/rio Often the based monad is fixed to be `IO` for simplicity, but you can also abstract over it like in your case. Being polymorphic over the base monad gives you more guarantees that your "program logic" won't have any effects other than those coming from the environment itself. – danidiaz Aug 24 '22 at 12:50
  • 1
    Possibly related: https://stackoverflow.com/questions/61780295/readert-design-pattern-parametrize-the-environment – danidiaz Aug 24 '22 at 12:51

1 Answers1

2

I'm not sure it's worth defining a separate transformer in this case. Consider that if you were using a reader transformer with an Int environment, you probably wouldn't feel a need to write a special transformer for this case:

newtype IntReaderT m a = IntReaderT (ReaderT Int m a)

Instead, you'd just use the ReaderT Int directly.

Similarly, if you happen to want a monadic action as your environment, I think you should just use the ReaderT directly, defining your monad stack as either a type:

type M = ReaderT (IO Token) IO

or a newtype:

newtype M a = M { unM :: ReaderT (IO Token) IO a }

along with a helper to fetch the token:

token :: M Token
token = ask >>= liftIO

If you prefer to write in a style that makes heavy use of mtl constraints, you can instead define token using a class and instance:

class MonadToken token m where
  token :: m token
instance MonadToken Token M where
  token = ask >>= liftIO

which allows you to write generic token-using monadic actions:

authorized :: (MonadToken token m) => m Bool
authorized = do
  t <- token
  ...

without actually having to define a new transformer.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71