4

It doesn't look like anything stops you from defining a function like this:

tmp :: (MonadReader Int m, MonadReader Bool m) => m Int
tmp = ifM ask ((+1) <$> ask) ((+2) <$> ask)

So would be something like a typeable context, where as long as you give it a type, ask will give you the part of the environment with that type, which I think is a reasonable alternative to name shadowing.

However, with the default instances of MonadReader defined by Control.Monad.Reader, it doesn't look like there's a way to actually invoke this method. So I defined a new module that attempts to do this:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses,
  UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Control.Monad.Reader.Extra
  ( MonadReader(ask)
  ) where

import Control.Monad.Reader (MonadReader(ask, local, reader))
import Control.Monad.Trans.Class (MonadTrans(lift))

instance {-# OVERLAPPABLE #-} ( Monad m
                              , MonadTrans t
                              , Monad (t m)
                              , MonadReader r m
                              ) =>
                              MonadReader r (t m) where
  ask = lift ask
  local _ _ = undefined
  reader f = lift (reader f)

As long as this second instance is in scope, you'll be able to run tmp, like runReaderT (runReaderT tmp 1) True.

Unfortunately, I can't figure out a decent implementation for local, since it needs to have the type local :: (r -> r) -> (t m a) -> (t m a), but it doesn't seem like there's a way to lift a local :: (r -> r) -> m a -> m a to this.

What would be a reasonable implementation for local for this instance?


Minimal Complete example:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances, LambdaCase, FlexibleContexts #-}
module Tmpfundep where

import Control.Monad.Reader
  ( MonadReader(ask, local, reader)
  , ReaderT(ReaderT)
  , runReaderT
  )

instance {-# INCOHERENT #-} (Monad m, MonadReader r m) =>
                            MonadReader r (ReaderT r' m) where
  ask = ReaderT (const ask)
  local f (ReaderT m) = ReaderT (local f . m)
  reader f = ReaderT (const (reader f))

withEnv :: r -> ReaderT r m a -> m a
withEnv r m = runReaderT m r

tmp :: (MonadReader Int m, MonadReader Bool m) => m Int
tmp = ask >>= \case
  True -> (+1) <$> ask
  False -> (+2) <$> ask

main :: IO ()
main = withEnv True (withEnv (1 :: Int) tmp) >>= print

Was able to run this with echo main | stack exec -- runghc Tmpfundep.hs

Rahul Manne
  • 1,229
  • 10
  • 20
  • 1
    You might be interested in this article: https://ro-che.info/articles/2014-07-15-type-based-lift – bradrn Feb 05 '21 at 23:53
  • 2
    `MonadReader` has a functional dependency that does, indeed, stop you from (usefully) writing `(MonadReader Int m, MonadReader Bool m)`. But see also [this package](http://hackage.haskell.org/package/effect-stack). – Daniel Wagner Feb 06 '21 at 00:57
  • 1
    @DanielWagner I think the FunDep issue is worth spelling out a lot more thoroughly - the fact that GHC will reject any attempt to create two different instances, even though it won't prevent writing a type that requires two different instances. – Carl Feb 06 '21 at 05:22
  • @DanielWagner Yep, looks like you're right. While this small example works for some reason, as the example gets more complex, it stops working. See https://stackoverflow.com/questions/7436109/haskell-functional-dependency-conflict/7436342#7436342. Though this doesn't explain why my trivial example, `tmp` DOES work. – Rahul Manne Feb 06 '21 at 19:05
  • @RahulManne Your `tmp` doesn't really work. You can't actually ever run it, not even by writing new `MonadReader` instances. – Daniel Wagner Feb 06 '21 at 19:06
  • It does work though. I literally ran print () on the constructed example. Let me put together a simple .hs for you. – Rahul Manne Feb 06 '21 at 19:08
  • @DanielWagner appended the working example. Aside from explicitly giving a type to "1" , everything else seems to just handles itself. The problem is when you call methods with these and relaxing constraints, like a method with (MonadReader x m, MonadReader y m) calling a different method with (MonadReader x m), but with more constraints. – Rahul Manne Feb 06 '21 at 19:55
  • @RahulManne Interesting. That's almost certainly a bug, as the `MonadReader` functional dependency definitely is violated by your new instance. The manual doesn't mention anything about incoherent instances allowing you to violate fundeps. I'd recommend you report it to the GHC folks -- I think they'll be very interested to see that. – Daniel Wagner Feb 06 '21 at 21:52
  • One way of getting around this limitation of `MonadReader` is having a `Has` two-parameter typeclass that would produce lenses to values of specific types present in the environment. – danidiaz Feb 10 '21 at 17:58

0 Answers0