Changing Reader environment type (in an effect system?)

If I have a function

foo :: (MonadReader a m) => m c

and I want to call it in from another function

bar :: (MonadReader (a, b) m) => m c

I think I need to ask first the value from the reader and then rewrap it with a ReaderT, as in

do
  (a, _) <- ask
  runReaderT foo a

IMHO, this is not really satisfactory, since it basically forces you to unwrap and rewrap a value. Moreover, as m gets more complex, also the implementation gets more complex, possibly with some lifting and some hoisting.

Is there a better way to approach this?
Is there out there an effect system which has a function similar to

enlarge :: (a -> b) -> Reader a es -> Reader b es

which would allow to have something like (using Bluefin-like syntax here)?

foo :: (e :> es) => Reader a e -> Eff es c

bar :: (e :> es) => Reader (a, b) e -> Eff es c
bar = foo . enlarge fst

Are you perhaps thinking of https://hackage-content.haskell.org/package/effectful-core-2.6.0.0/docs/Effectful-Reader-Dynamic.html#v:local ?

nope, because local does not change the type of the environment

If you are already using an optics library, you can use magnify.

that looks nice, but I’m not able to use it as simply as I’d like to

foo :: (MonadReader Int m) => m Bool

foo'' :: MonadReader (Int, Char) m => m Bool
foo'' = magnify _1 foo

produces

• Couldn't match type: Control.Lens.Zoom.Magnified m0
                     with: Control.Lens.Zoom.Magnified m
        arising from a superclass required to satisfy ‘Magnify
                                                         m0 m Int (Int, Char)’,
        arising from a use of ‘magnify’
        NB: ‘Control.Lens.Zoom.Magnified’ is a non-injective type family
        The type variable ‘m0’ is ambiguous

Am I forgetting something? m0 can not be m since they have a different environment

It should work with a concrete Reader (or a concrete transformer stack in that regard):

foo'' :: Reader (Int, Char) Bool
foo'' = magnify _1 foo

I think the functional dependencies on the Magnify class is not enough for GHC to infer Magnify m n Int (Int, Char) from MonadReader (Int, Char) n. If you would rather not provide a concrete monad for foo'', another solution could be to give the following signature to foo'', which should hopefully be resolved once the concrete monad is provided:

foo'' :: Magnify m n Int (Int, Char) => n Bool
foo'' = magnify _1 foo

Though I must admit that neither solution is very satisfying.

I do not think you can go from MonadReader (a,b) m to MonadReader a m in a sane manner. In theory, you could add an instance MonadReader (a,b) m => MonadReader a m, but this new instance would then overlap with all other instances. At the moment, instance resolution is not smart enough for such cases.

I think you can still get close to what you want. You need to run the MonadReader a m effect by relying on MonadReader (a,b) m.

If you use an effect system, you can try something like reinterpret (Control.Monad.Freer)

If you use transformers, you can write something like this:

foo :: (MonadReader Int m) => m Int
foo = ask

bar :: (MonadReader (Int, String) m) => m Int
bar = do
  (i, _) ← ask
  runReaderT foo i

EDIT: Sorry, I have seen just now that this is what you already tried.

For Bluefin specifically, I would try something like this:

data MyReader a e = MyReader (Reader x e) (x -> a)

Then you can implement the enlarge function, though you would also need to add custom run and ask functions.

I think Effectful’s version is called withReader but the haddocks seem to state it’s deprecated/buggy. mtl has a ‘withReader’ specifically for Reader and ReaderT, but not classy version it seems.

edit: the static interpreter for Reader apparently has a version that not deprecated

Ah in fact I was looking not for local but for the Static Reader’s version of withReader, which does not appear to suffer from the problem.

ah, thanks @noinia and @Kleidukos. That looks exactly like what I want! I’ll give it a try

actually it’s not…

withReader assumes that Reader is the first effect in your stack (similarly to what the original withReader assumes).

I would like something like

withReader
  :: (HasCallStack, Reader r2 :> es, Reader r1 :> es')
  => (r1 -> r2)
  -- ^ The function to modify the environment.
  -> Eff es a
  -- ^ Computation to run in the modified environment.
  -> Eff es' a

but I guess that does not currently exist

P.S. I guess the constraints list should also contain something saying that every other effect in es is contained in es'

withReader in effectful can be slightly generalized to

-- | Execute a computation in a modified environment.
--
-- @since 1.1.0.0
withReader
  :: (HasCallStack, Reader r1 :> es)
  => (r1 -> r2)
  -- ^ The function to modify the environment.
  -> Eff (Reader r2 : es) a
  -- ^ Computation to run in the modified environment.
  -> Eff es a
withReader f m = do
  r <- ask
  runReader (f r) m

but it can’t have the type signature you wrote.

Yes, withReader is essentially a handler of its Readerargument. The Bluefin version would be a direct correspondence:

withReader ::
  e1 :> es =>
  (r1 -> r2) ->
  Reader r1 e1 ->
  (forall e. Reader r2 e -> Eff (e :& es) a) ->
  Eff es a

[EDIT: fixed thanks to @raehik]

Did you miss e1 off the end of Reader r1in the second position? (Sorry to nitpick.)

Yes I did, now fixed, thanks! No need to apologise. It can be very confusing when these things are subtly wrong, so it’s important to get them right and I didn’t bother to test it :slight_smile:

Are you familiar with the HasX pattern? Instead of

foo :: (MonadReader A m) => m c

bar :: (MonadReader (A, B) m) => m c

You have

class HasA env where
  getA :: env -> A

class HasB env where
  getB :: env -> B

foo :: (HasA env, MonadReader env m) => m c

bar :: (HasA env, HasB env, MonadReader env m) => m c

Then bar can call foo without any extra ceremony. You can use this pattern with mtl or effects libraries, though the latter may require manual type annotations due to inference woes e.g. in effectful:


baz :: forall env es. (HasA env, HasB env, Reader env :> es) => Eff es ()
baz = do
  a <- asks @env getA
  ...

There is even a package for this, though imo it is somewhat superceded by optics and HasField, which are an alternative to manual HasX classes. The key point is keeping the env in MonadReader env abstract.

The issue I see in such a solution is that bar needs to know about A through the HasA constraint.

That would go away having an instance for HasA everytime we have an instance for HasB and a function B -> A, but I don’t that a good approach using typeclasses

I got the idea for something like this but I don’t know how practical it is

class (MonadReader r m, forall r' . MonadReader r' (ChangeReader m r')) => SmartMonadReader r m where
  data family ChangeReader m r' :: Type -> Type
  changeReader :: (r' -> r) -> m a -> ChangeReader m r' a
  

instance Monad m => SmartMonadReader r (ReaderT r m) where
  newtype instance ChangeReader (ReaderT r m) r' a = ChangeReaderReaderT (ReaderT r' m a)
    deriving newtype (Functor, Applicative, Monad, MonadReader r')
  changeReader f x = ChangeReaderReaderT (withReaderT f x)


instance (SmartMonadReader r m) => SmartMonadReader r (MaybeT m) where
  newtype instance ChangeReader (MaybeT m) r' a = ChangeReaderMaybeT (MaybeT (ChangeReader m r') a)
  changeReader f x = ChangeReaderMaybeT (mapMaybeT (changeReader f) x)

deriving newtype instance (SmartMonadReader r m) => Functor (ChangeReader (MaybeT m) r')
deriving newtype instance (SmartMonadReader r m) => Applicative (ChangeReader (MaybeT m) r')
deriving newtype instance (SmartMonadReader r m) => Monad (ChangeReader (MaybeT m) r')
deriving newtype instance (SmartMonadReader r m) => MonadReader r' (ChangeReader (MaybeT m) r')


instance (Monoid w, SmartMonadReader r m) => SmartMonadReader r (WriterT w m) where
  newtype instance ChangeReader (WriterT w m) r' a = ChangeReaderWriterT (WriterT w (ChangeReader m r') a)
  changeReader f x = ChangeReaderWriterT (mapWriterT (changeReader f) x)

deriving newtype instance (Monoid w, SmartMonadReader r m) => Functor (ChangeReader (WriterT w m) r')
deriving newtype instance (Monoid w, SmartMonadReader r m) => Applicative (ChangeReader (WriterT w m) r')
deriving newtype instance (Monoid w, SmartMonadReader r m) => Monad (ChangeReader (WriterT w m) r')
deriving newtype instance (Monoid w, SmartMonadReader r m) => MonadReader r' (ChangeReader (WriterT w m) r')