Reputation: 9376
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
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
.
(->) e
, which is what the function r
usesReaderT e m
, a newtype wrapper for functions of type e -> m a
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
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
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
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