Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is FunctionalDependency needed for defining MonadReader?

I just managed to understand the definition of the class MonadReader

class Monad m => MonadReader r m | m -> r where
...

After reading the document of Functional Dependency in Haskell, now I can understand that | m -> r specifies that type variable r is uniquely decided by m. I think this requirement is reasonable based on the few typical instances of MonadReader I have seen so far (e.g. Reader), but it seems to me that we can still define instances like Reader even without this functional dependency clause.

My question is why we need functional dependency in the definition of MonadReader? Is this functionally necessary for defining MonadReader in a sense that MonadReader cannot be properly defined without it, or it is merely a restriction to limit the ways how MonadReader can be used so that the instances of MonadReader will all behave in a certain expected way?

like image 573
Lifu Huang Avatar asked Nov 20 '19 05:11

Lifu Huang


3 Answers

It is needed to make type inference work in a way which is more convenient to the user.

For example, without the fundep this would not compile:

action :: ReaderT Int IO ()
action = do
  x <- ask
  liftIO $ print x

To make the above compile we would need to write

action :: ReadertT Int IO ()
action = do
  x <- ask :: ReadertT Int IO Int
  liftIO $ print x

This is because, without the fundep, the compiler can not infer that x is an Int. After all a monad ReadertT Int IO might have multiple instances

instance MonadReader Int (ReaderT Int IO) where
   ask = ReaderT (\i -> return i)
instance MonadReader Bool (ReaderT Int IO) where
   ask = ReaderT (\i -> return (i != 0))
instance MonadReader String (ReaderT Int IO) where
   ask = ReaderT (\i -> return (show i))
-- etc.

so the programmer must provide some annotation which forces x :: Int, or the code is ambiguous.

like image 81
chi Avatar answered Nov 13 '22 07:11

chi


This is not really an answer, but it's much too long for a comment. You are correct that it's possible to define the MonadReader class without a fundep. In particular, the type signature of each method determines every class parameter. It would be quite possible to define a finer hierarchy.

class MonadReaderA r m where
  askA :: m r
  askA = readerA id

  readerA :: (r -> a) -> m a
  readerA f = f <$> askA

-- This effect is somewhat different in
-- character and requires special lifting.
class MonadReaderA r m => MonadReaderB r m where
  localB :: (r -> r) -> m a -> m a

class MonadReaderB r m
  => MonadReader r m | m -> r

ask :: MonadReader r m => m r
ask = askA

reader
  :: MonadReader r m
  => (r -> a) -> m a
reader = readerA

local
  :: MonadReader r m
  => (r -> r) -> m a -> m a
local = localB

The main problem with this approach is that users have to write a bunch of instances.

like image 41
dfeuer Avatar answered Nov 13 '22 07:11

dfeuer


I think the source of confusion is that in the definition of

class Monad m => MonadReader r m | m -> r where
  {- ... -}

It is implicitly asumed that m contains r itself (for common instances). Let me use a lighter definiton of Reader as

newtype Reader r a = Reader {runReader :: r -> a}

When the r parameter is chosen you can easely define a monad instance for Reader r. That means that in the type class definition m should be substitute for Reader r. So take a look at how the expresion ends up being:

instance MonadReader r (Reader r) where -- hey!! r is duplicated now
  {- ... -}                             -- The functional dependecy becomes Reader r -> r which makes sense

But why do we need this?. Look at the definition of ask within the MonadReader class.

class Monad m => MonadReader r m | m -> r where
  ask :: m r -- r and m are polymorphic here
  {- ... -}

Without the fun-dep nothing could stop me for defining ask in a way to return a different type as the state. Even more, I could define many instances of monad reader for my type. As an example, this would be valid definitions without func-dep

instance MonadReader Bool (Reader r) where
--                   ^^^^         ^
--                   |            |- This is state type in the user defined newtype 
--                   |- this is the state type in the type class definition
  ask :: Reader r Bool
  ask = Reader (\_ -> True) -- the function that returns True constantly
  {- ... -}                             
instance MonadReader String (Reader r) where
--                   ^^^^^^         ^
--                   |              |- This is read-state type in the user defined newtype 
--                   |- this is the read-state type in the type class definition
  ask :: Reader r String
  ask = Reader (\_ -> "ThisIsBroken") -- the function that returns "ThisIsBroken" constantly
  {- ... -}                             

So if I had a value val :: ReaderT Int IO Double what would be the result of ask. We'd need to specify a type signature as below

val :: Reader Int Double
val = do
  r <- ask :: Reader Int String
  liftIO $ putStrLn r   -- Just imagine you can use liftIO
  return 1.0

> val `runReader` 1
"ThisIsBroken"
1.0

val :: Reader Int Double
val = do
  r <- ask :: Reader Int Bool
  liftIO $ print r   -- Just imagine you can use liftIO
  return 1.0

> val `runReader` 1
True
1.0

Aside from being senseless, it is unconvinient to be specifiying the type over and over.

As a conclusion using the actual definition of ReaderT. When you have something like val :: ReaderT String IO Int the functional dependency says Such a type might have only one single instance of MonadReader typeclass which is defined to be the one that uses String as r

like image 28
lsmor Avatar answered Nov 13 '22 06:11

lsmor