ron
ron

Reputation: 9376

Why does the following ReaderT String IO lose an IO action?

module Main (main) where

import Control.Monad.Reader

p1 :: String -> IO ()
p1 = putStrLn . ("Apple "++)

p2 :: String -> IO ()
p2 = putStrLn . ("Pear "++)

main :: IO ()
main = do
    p1 "x"
    p2 "y"
    r "z"

r :: String -> IO ()
r = do
    p1
    p2

It prints:

Apple x Pear y Pear z

Why?

Upvotes: 1

Views: 242

Answers (4)

Vitus
Vitus

Reputation: 11922

The problem is in r. Given the following definition of Reader monad:

instance Monad ((->) e) where
    return = const
    f >>= g = \x -> g (f x) x

We can simplify r:

r = p1 >> p2    
  = (>>=) p1 (\_ -> p2)    
  = (\f g x -> g (f x) x) p1 (\_ -> p2)    
  = \x -> (\_ -> p2) (p1 x) x    
  = \x -> p2 x

This also shows that Reader's (>>) is just const with a bit more specific type.

If you want to distribute the environment and then execute both actions, you have to bind the result of applying p1 to the environment, for example:

r = do a1 <- p1
       a2 <- p2
       return (a1 >> a2)

Or using Applicative:

r = (>>) <$> p1 <*> p2

Expanding on the Reader part, Control.Monad.Reader provides three variants of Reader.

  • the implicit (->) e, which is what the function r uses
  • the monad transformer ReaderT e m, a newtype wrapper for functions of type e -> m a
  • the explicit Reader e, defined in terms of ReaderT as ReaderT e Identity

Without any further information, the implicit (->) e will be used. Why?

The overall type of do block is given by the last expression, which is also constrained to be of the form Monad m => m a for some m and a.

Looking back at r, it's clear that the do block has a type String -> IO () as given by the type of r and also p2. It also requires String -> IO () to be Monad m => m a. Now, unifying these two types:

m = (->) String
a = IO ()

This matches (->) e monad instance by choosing e = String.


Being a monad transformer, ReaderT takes care of the inner plumbing to make sure the actions of the inner monad are properly sequenced and executed. To select ReaderT, it is necessary to explicitly mention it (usually in a type signature, but functions which fix the type to be ReaderT, such as runReaderT, also work):

r :: ReaderT String IO ()
r = do ? p1
       ? p2

r' :: String -> IO ()
r' = runReaderT r

This comes with another problem, p1 and p2 have a type String -> IO (), which doesn't match the required ReaderT String IO ().

The ad-hoc solution (tailored exactly for this situation), is just to apply

ReaderT :: (e -> m a) -> ReaderT e m a

To obtain something more general, MonadIO type class can lift IO actions into the transformer and MonadReader type class allows accessing the environment. These two type classes work as long as there is IO (or ReaderT respectively) somewhere in the transformer stack.

lift' :: (MonadIO m, MonadReader a m) => (a -> IO b) -> m b
lift' f = do
    env <- ask     -- get environment
    let io = f env -- apply f to get the IO action
    liftIO io      -- lift IO action into transformer stack

Or more concisely:

lift' f = ask >>= liftIO . f

Regarding your question in comments, you can implement the relevant instances in this way:

newtype ReaderT e m a = ReaderT { runReaderT :: e -> m a }

instance Monad m => Monad (ReaderT e m) where
    return  = ReaderT . const . return
      -- The transformers package defines it as "lift . return".
      -- These two definitions are equivalent, though.
    m >>= f = ReaderT $ \e -> do
        a <- runReaderT m e
        runReaderT (f a) e

instance Monad m => MonadReader e (ReaderT e m) where
    ask        = ReaderT return
    local  f m = ReaderT $ runReaderT m . f
    reader f   = ReaderT (return . f)

The actual typeclass can be found in the mtl package (package, type class), the newtype and Monad instance in transformers package (package, type class).


As for making a e -> m a Monad instance, you are out of luck. Monad requires a type constructor of kind * -> *, which means we are attempting to do something like this (in pseudo-code):

instance Monad m => Monad (/\a -> e -> m a) where
    -- ...

where /\ stands for type-level lambda. However, the closest thing we can get to a type level lambda is a type synonym (which must be fully applied before we can make type class instances, so no luck here) or a type family (which cannot be used as an argument to type class either). Using something like (->) e . m leads to newtype again.

Upvotes: 7

Chris Kuklewicz
Chris Kuklewicz

Reputation: 8153

For r you used (->) String (IO ()) which is a Monad ((->) String) that returns a value of type IO ().

You did NOT use a ReaderT or any monad transformer. You used a monad that returns a different monad. It accidentally compiled and ran, almost doing what you expected.

You need to use runReaderT and lift (or liftIO) to achieve the r that I think you are trying to make.

Upvotes: 2

Daniel Fischer
Daniel Fischer

Reputation: 183968

Let's first rewrite the body of

r :: String -> IO ()
r = do
    p1
    p2

using (>>),

r = p1 >> p2

so p1 must have type m a for some Monad m, and p2 must have type m b for the same m.

Now,

p1, p2 :: String -> IO ()

and the top-level type constructor in that is the function arrow (->). Therefore the Monad used in r must be

(->) String

The Monad instance for (->) e [aka the reader monad], is

instance Monad ((->) e) where
    -- return :: a -> (e -> a)
    return = const
    -- (>>=) :: (e -> a) -> (a -> (e -> b)) -> (e -> b)
    f >>= g = \x -> g (f x) x

and consequently,

p1 >> p2 = p1 >>= \_ -> p2
         = \x -> (\_ -> p2) (p1 x) x   -- apply (\_ -> p2) to (p1 x)
         = \x -> p2 x                  -- eta-reduce
         = p2

so that was just a complicated way to write

r = p2

Upvotes: 2

mhwombat
mhwombat

Reputation: 8136

You left off the argument when you invoke p1 and p2 in r. What you wrote is then interpreted as pointfree notation, so only the second IO action gets an argument. This works:

r :: String -> IO ()
r x = do
    p1 x
    p2 x

To understand why this is happening, consider that what you originally wrote is equivalent to

r = p1 >> p2

The compiler interprets that as something like

r x = (p1 >> p2) x

Which isn't what you want.

Upvotes: 0

Related Questions