danidiaz
danidiaz

Reputation: 27766

Writing a "zooming" function for a ReaderT-like monad transformer

I have this ReaderT-like monad transformer (inspired by this answer):

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneKindSignatures #-}
import Control.Monad.Reader -- from "mtl"
import Data.Kind (Type)

type DepT :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type
newtype DepT env m r = DepT {toReaderT :: ReaderT (env (DepT env m)) m r}
  deriving (Functor, Applicative, Monad, MonadReader (env (DepT env m)))

instance MonadTrans (DepT env) where
  lift = DepT . lift

And these two parameterized records, to which I give "rank-2 functor" instances:

{-# LANGUAGE TemplateHaskell #-}
import qualified Rank2 -- form rank2classes
import qualified Rank2.TH

type Env :: (Type -> Type) -> Type
data Env m = Env
  { logger :: String -> m (),
    logic :: Int -> m Int
  }
$(Rank2.TH.deriveFunctor ''Env)

type BiggerEnv :: (Type -> Type) -> Type
data BiggerEnv m = BiggerEnv
  { inner :: Env m,
    extra :: Int -> m Int
  }
$(Rank2.TH.deriveFunctor ''BiggerEnv)

Intuitively, I expect to be able to write a conversion function with the type:

zoom :: forall a. DepT Env IO a -> DepT BiggerEnv IO a

This is because DepT Env IO a works with "less info" than DepT BiggerEnv IO a.

But I'm stuck. Is there a way to write zoom?

Upvotes: 0

Views: 168

Answers (2)

danidiaz
danidiaz

Reputation: 27766

The general solution would be a function like:

withDepT ::
  forall small big m a.
  Monad m =>
  ( forall p q.
    (forall x. p x -> q x) ->
    small p ->
    small q
  ) ->
  (forall t. big t -> small t) ->
  DepT small m a ->
  DepT big m a
withDepT mapEnv inner (DepT (ReaderT f)) =
  DepT
    ( ReaderT
        ( \big ->
            let small :: small (DepT small m)
                -- we have a big environment at hand, so let's extract the
                -- small environment, transform every function in the small
                -- environment by supplying the big environment and, as a
                -- finishing touch, lift from the base monad m so that it
                -- matches the monad expected by f.
                small = mapEnv (lift . flip runDepT big) (inner big)
             in f small
        )
    )

Where the first argument would be, in the case of Env, a function like

mapEnv :: (forall x. n x -> m x) -> Env n -> Env m
mapEnv f (Env {logger,logic}) =
    Env { logger = f . logger, logic = f . logic }

which changes the monad of the environment. mapEnv corresponds to Rank2.<$> from rank2classes.

Upvotes: 1

castletheperson
castletheperson

Reputation: 33496

First, we could create a more general function, withDepT, which is similar to the withReaderT function.

withDepT :: forall env env' m a.
            (env' (DepT env' m) -> env (DepT env m))
            -> DepT env m a
            -> DepT env' m a
withDepT f (DepT m) = DepT (withReaderT f m)

And then we can use this to implement zoom by providing a function like:

biggerEnvToEnv :: BiggerEnv (DepT BiggerEnv IO) -> Env (DepT Env IO)
biggerEnvToEnv (BiggerEnv (Env logger logic) _) = Env logger' logic'
  where
    logger' = mystery . logger
    logic' = mystery . logic

zoom = withDepT biggerEnvToEnv

But then we need to implement mystery. Let's look at its type:

mystery :: forall a. DepT BiggerEnv IO a -> DepT Env IO a

Now we can see that mystery is the opposite of our desired zoom function:

zoom :: forall a. DepT Env IO a -> DepT BiggerEnv IO a

So we can conclude that it's impossible to naturally derive zoom unless BiggerEnv and Env are isomorphic, which they're not because of the extra value in BiggerEnv.

Upvotes: 2

Related Questions