Generalize types of IO actions
One nice way to structure programs is to use monad transformers on top of IO. For example, one common technique is to use StateT or ReaderT to propagate custom state through IO code. This technique has two problems
-
It requires one to use 'liftIO' rather a lot, which is mildly irritating
-
It makes it very difficult to use higher-order IO combinators, such as try or bracket, which is highly irritating
This proposal involves generalizing the types of standard IO functions to make this use-case easier.
Conservative proposal:
Generalize higher order IO combinators to take a MonadIO context. For example, try and catch would then have types
try :: MonadIO m => m a -> m (Either Exception a)
catch :: MonadIO m => m a -> (Exception -> m a) - > m a
This allows one to use custom monads built on IO and still do correct exception handling, etc. without having to do lots of nasty monad unwrapping/rewrapping.
NOTE: This is not possible to implement. there is no way to 'rewind' a monad in a generic way in order to implement try or catch. - JohnMeacham
NOTE: An alternative is to use a different version of Control.Exception that defines a typeclass as follows:
class MonadIO m => MonadException m where
catch :: m a -> (Exception -> m a) -> m a
block, unblock :: m a -> m a
This can be implemented for the common monad transformers, and all the other exception functions can be implemented just using the methods in the above typeclass, or using liftIO with the existing functions. A full implementation (not fully tested though) is attached. -- Brian Hulley
NOTE: Even though not all uses of IO can be eliminated (eg when defining callback functions to use with FFI), the importance of having an Exception module which is not tied to concrete IO
is that it would allow library writers which require block
, bracket
etc to use the more general MonadException
in place of IO
. If library writers do not do this, users of the library, which may be other libraries etc, are also tied down to concrete IO
, so the longer we leave it, the more difficult it will be to "unconcretise" the code base. -- Brian Hulley
More radical proposal:
Generalize the types of all standard library IO routines to take MonadIO contexts. For example,
putChar :: MonadIO m => Char -> m ()
getChar :: MonadIO m => m Char
These could easily be defined in terms of lower level IO primitives:
putCharIO :: Char -> IO ()
putCharIO = ...
putChar c = liftIO (putChar c)
Now you can eliminate a bunch of noisy calls to liftIO in client code.
NOTE: This should be quite trivial to implement for all functions which just use IO
in the return value. When IO
is used in other positions it is unlikely to be possible to implement except by replacing the other occurrence of IO
by MonadIOU
defined by:
class MonadIO m => MonadIOU m where
getUnliftIO :: m (m a -> IO a)
Unfortunately only a few of the monad transformers support this operation (eg ReaderT
) but at least it's better than nothing. An alternative would be to define special typeclasses for related sets of operations if this would allow more monad transformers to be supported by the particular operations, or to add first-order api functions which a library user could use to build a specialised version of the higher order function for a specific monad. -- Brian Hulley