IO
transformerIt has been
claimed
that there is no IO
transformer in Haskell. This seems overly
pessimistic. The obvious approach is to use
newtype IOT m a = IOT (m (IO a))
However, we get stuck when we try to define
join :: Monad m => m (IO (m (IO a))) -> m (IO a)
The typical approach would be to try to commute the IO (m ...)
to m (IO ...)
but there’s just no way to do that. We can’t even do it for
some simple choices of m
like Maybe
let alone uniformly for all
Monad
s m
.
But there’s a another way. If we can turn all the m
s into IO
then
we can just join them all together into one IO
action. This
suggests an IO
transformer like
newtype IOT m a = IOT ((forall a. m a -> IO a) -> IO a)
In fact, it’s just a use of the Reader
transformer.
newtype Morph f g = Morph { runMorph :: forall a. (f a -> g a) }
newtype IOT m a = IOT { runIOT :: ReaderT (Morph m IO) IO a }
Using ReaderT
here is just a convenience that means we automatically
get a Monad
instance cheaply. It doesn’t play a role of any substance.
instance Monad (IOT m) where
return = IOT . return
m >>= f = IOT (runIOT m >>= (runIOT . f))
The MonadTrans
instance is
instance MonadTrans IOT where
lift act = IOT $ do
f <- ask
lift (runMorph f act)
That is, we just convert all the m
actions into IO
actions. (Note
that the latter lift
is in ReaderT
). Does it satisfy the
MonadTrans
laws?
Well (eliding runMorph
and IOT
wrapping/unwrapping for notational
convenience),
lift (return x) = do
f <- ask
lift (f (return x))
which is
lift (return x) = do
f <- ask
return x
in the case where f (return x)
= return x
, i.e. lift . return = return
. Additionally
lift (m >>= g) = do
f <- ask
lift (f (m >>= g))
which is
lift (m >>= g) = do
f <- ask
x <- lift (f m)
f' <- ask
lift (f' (g x))
i.e.
lift m >>= \x -> lift (g x)
that is
lift m >>= (lift . g)
in the case where f (m >>= g) = f m >>= (f . g)
. So lift (m >>= g) = lift m >>= (lift . g)
.
Thus we see that IOT
is a monad transformer if the reader
environment is a monad morphism f
satisfying
f (return x) = return x
f (m >>= g) = f m >>= (f . g)
IOT
You can run an IOT m
action by providing a way of interpreting an
m
action in IO
. (Remember the interpretation has to be a monad
morphism for the monad laws to hold).
runIOT' :: (forall a. m a -> IO a) -> IOT m a -> IO a
runIOT' f m = runReaderT (runIOT m) (Morph f)
It seems that there are several uses of this transformer.
Maybe
For m = Maybe
we can throw an error for Nothing
.
handleMaybe :: Maybe a -> IO a
handleMaybe Nothing = error "Nothing"
handleMaybe (Just a) = return a
maybeExample :: IO ()
maybeExample = runIOT' handleMaybe $ do
liftIO (putStrLn "Hello")
lift Nothing
liftIO (putStrLn "Goodbye")
> maybeExample
Hello
*** Exception: Nothing
Either
For m = Either e
we can throw an error which depends on the
Left
value.
handleEither :: Show e => Either e a -> IO a
handleEither (Left e) = error (show e)
handleEither (Right a) = return a
eitherExample :: IO ()
eitherExample = runIOT' handleEither $ do
liftIO (putStrLn "Hello")
lift (Left 1234)
liftIO (putStrLn "Goodbye")
> eitherExample
Hello
*** Exception: 1234
Writer
For m = Writer [w]
we can print a log message.
handleWriter :: Show w => Writer [w] a -> IO a
handleWriter m = mapM_ print ws >> return a
where (a, ws) = runWriter m
writerExample :: IO ()
writerExample = runIOT' handleWriter $ do
liftIO (putStrLn "Hello")
lift (tell ["log1", "log2"])
liftIO (putStrLn "Goodbye")
lift (tell ["log3"])
> writerExample
Hello
"log1"
"log2"
Goodbye
"log3"
State s
For m = State s
we can use an IORef
to hold the state.
stateExample :: IO ()
stateExample = do
ref <- newIORef 0
let handleState :: State Integer a -> IO a
handleState m = do
s <- readIORef ref
let (a, s') = runState m s
writeIORef ref s'
return a
runIOT' handleState $ do
x <- lift get
liftIO (print x)
lift (modify (+1))
y <- lift get
liftIO (print y)
> stateExample
0
1
IO
transformer?Firstly note that IOT Maybe
, IOT (Either e)
and IOT (State s)
seem to give strictly less power than MaybeT IO
, EitherT e IO
and
StateT s IO
respectively. IOT (Writer [w])
does seem to be
different to WriterT [w] IO
in that it preserves the ordering of
IO
and Writer
actions.
But is there some definition for “M
transformer”, for an arbitrary
monad M
? And what is the justification for saying that Haskell has
no IO
transformer? Even though it may not be especially useful,
this IOT
seems like a reasonable candidate for one.
{-# LANGUAGE Rank2Types #-}
import Control.Monad.Trans.Reader
import Control.Monad.Trans.Writer
import Control.Monad.Trans.State
import Control.Monad.Trans.Class
import Data.IORef
newtype Morph f g = Morph { runMorph :: forall a. (f a -> g a) }
newtype IOT m a = IOT { runIOT :: ReaderT (Morph m IO) IO a }
instance Monad (IOT m) where
return = IOT . return
m >>= f = IOT (runIOT m >>= (runIOT . f))
instance MonadTrans IOT where
lift act = IOT $ do
f <- ask
lift (runMorph f act)
liftIO :: IO a -> IOT m a
liftIO = IOT . lift
handleMaybe :: Maybe a -> IO a
handleMaybe Nothing = error "Nothing"
handleMaybe (Just a) = return a
runIOT' :: (forall a. m a -> IO a) -> IOT m a -> IO a
runIOT' f m = runReaderT (runIOT m) (Morph f)
maybeExample :: IO ()
maybeExample = runIOT' handleMaybe $ do
liftIO (putStrLn "Hello")
lift Nothing
liftIO (putStrLn "Goodbye")
handleEither :: Show e => Either e a -> IO a
handleEither (Left e) = error (show e)
handleEither (Right a) = return a
eitherExample :: IO ()
eitherExample = runIOT' handleEither $ do
liftIO (putStrLn "Hello")
lift (Left 1234)
liftIO (putStrLn "Goodbye")
handleWriter :: Show w => Writer [w] a -> IO a
handleWriter m = mapM_ print ws >> return a
where (a, ws) = runWriter m
writerExample :: IO ()
writerExample = runIOT' handleWriter $ do
liftIO (putStrLn "Hello")
lift (tell ["log1", "log2"])
liftIO (putStrLn "Goodbye")
lift (tell ["log3"])
stateExample :: IO ()
stateExample = do
ref <- newIORef 0
let handleState :: State Integer a -> IO a
handleState m = do
s <- readIORef ref
let (a, s') = runState m s
writeIORef ref s'
return a
runIOT' handleState $ do
x <- lift get
liftIO (print x)
lift (modify (+1))
y <- lift get