Combining ReaderT monads? - haskell

It seems that it would be useful to be able to combine different ReaderT environments.
For instance, a generic logging facility might look something like this:
logit :: Text -> ReaderT Bool IO ()
logit str = do debugflag <- ask
liftIO $ if debugflag then putStrLn ("debug: " ++ str) else return ()
This looks like a nice reusable component. So how would I go about integrating this definition with another ReaderT environment so that I could use both of them?
For instance, suppose I want to combine it with this ReaderT instance:
foo :: ReaderT Text IO ()
foo = ...
so that I can use both foo and logit in the same function.

You'll want to layer them into a stacked monad, but they can't be stacked together since both of them declare that IO is exactly the wrapped monad. Fortunately, your code is already general enough to lift this restriction. The most general types of your functions use MonadIO instead of specifically using IO. If you change the types to
logit :: MonadIO m => Text -> ReaderT Bool m ()
foo :: MonadIO m => ReaderT Text m ()
then the liftIO call will lift the IO actions through the entire stack to an IO monad at the bottom.
To be clear, the types you've written do not need to use liftIO—the same type would be satisfied by just lift, but since IO is (trivially) an instance of MonadIO then your (overly) specialized type will also pass the checker.

Related

How to understand `MonadUnliftIO`'s requirement of "no stateful monads"?

I've looked over https://www.fpcomplete.com/blog/2017/06/tale-of-two-brackets, though skimming some parts, and I still don't quite understand the core issue "StateT is bad, IO is OK", other than vaguely getting the sense that Haskell allows one to write bad StateT monads (or in the ultimate example in the article, MonadBaseControl instead of StateT, I think).
In the haddocks, the following law must be satisfied:
askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m
So this appears to be saying that state is not mutated in the monad m when using askUnliftIO. But to my mind, in IO, the entire world can be the state. I could be reading and writing to a text file on disk, for instance.
To quote another article by Michael,
False purity We say WriterT and StateT are pure, and technically they
are. But let's be honest: if you have an application which is entirely
living within a StateT, you're not getting the benefits of restrained
mutation that you want from pure code. May as well call a spade a
spade, and accept that you have a mutable variable.
This makes me think this is indeed the case: with IO we are being honest, with StateT, we are not being honest about mutability ... but that seems another issue than what the law above is trying to show; after all, MonadUnliftIO is assuming IO. I'm having trouble understanding conceptually how IO is more restrictive than something else.
Update 1
After sleeping (some), I am still confused but am gradually getting less so as the day wears on. I worked out the law proof for IO. I realized the presence of id in the README. In particular,
instance MonadUnliftIO IO where
askUnliftIO = return (UnliftIO id)
So askUnliftIO would appear to return an IO (IO a) on an UnliftIO m.
Prelude> fooIO = print 5
Prelude> :t fooIO
fooIO :: IO ()
Prelude> let barIO :: IO(IO ()); barIO = return fooIO
Prelude> :t barIO
barIO :: IO (IO ())
Back to the law, it really appears to be saying that state is not mutated in the monad m when doing a round trip on the transformed monad (askUnliftIO), where the round trip is unLiftIO -> liftIO.
Resuming the example above, barIO :: IO (), so if we do barIO >>= (u -> liftIO (unliftIO u m)), then u :: IO () and unliftIO u == IO (), then liftIO (IO ()) == IO (). **So since everything has basically been applications of id under the hood, we can see that no state was changed, even though we are using IO. Crucially, I think, what is important is that the value in a is never run, nor is any other state modified, as a result of using askUnliftIO. If it did, then like in the case of randomIO :: IO a, we would not be able to get the same value had we not run askUnliftIO on it. (Verification attempt 1 below)
But, it still seems like we could do the same for other Monads, even if they do maintain state. But I also see how, for some monads, we may not be able to do so. Thinking of a contrived example: each time we access the value of type a contained in the stateful monad, some internal state is changed.
Verification attempt 1
> fooIO >> askUnliftIO
5
> fooIOunlift = fooIO >> askUnliftIO
> :t fooIOunlift
fooIOunlift :: IO (UnliftIO IO)
> fooIOunlift
5
Good so far, but confused about why the following occurs:
> fooIOunlift >>= (\u -> unliftIO u)
<interactive>:50:24: error:
* Couldn't match expected type `IO b'
with actual type `IO a0 -> IO a0'
* Probable cause: `unliftIO' is applied to too few arguments
In the expression: unliftIO u
In the second argument of `(>>=)', namely `(\ u -> unliftIO u)'
In the expression: fooIOunlift >>= (\ u -> unliftIO u)
* Relevant bindings include
it :: IO b (bound at <interactive>:50:1)
"StateT is bad, IO is OK"
That's not really the point of the article. The idea is that MonadBaseControl permits some confusing (and often undesirable) behaviors with stateful monad transformers in the presence of concurrency and exceptions.
finally :: StateT s IO a -> StateT s IO a -> StateT s IO a is a great example. If you use the "StateT is attaching a mutable variable of type s onto a monad m" metaphor, then you might expect that the finalizer action gets access to the most recent s value when an exception was thrown.
forkState :: StateT s IO a -> StateT s IO ThreadId is another one. You might expect that the state modifications from the input would be reflected in the original thread.
lol :: StateT Int IO [ThreadId]
lol = do
for [1..10] $ \i -> do
forkState $ modify (+i)
You might expect that lol could be rewritten (modulo performance) as modify (+ sum [1..10]). But that's not right. The implementation of forkState just passes the initial state to the forked thread, and then can never retrieve any state modifications. The easy/common understanding of StateT fails you here.
Instead, you have to adopt a more nuanced view of StateT s m a as "a transformer that provides a thread-local immutable variable of type s which is implicitly threaded through a computation, and it is possible to replace that local variable with a new value of the same type for future steps of the computation." (more or less a verbose english retelling of the s -> m (a, s)) With this understanding, the behavior of finally becomes a bit more clear: it's a local variable, so it does not survive exceptions. Likewise, forkState becomes more clear: it's a thread-local variable, so obviously a change to a different thread won't affect any others.
This is sometimes what you want. But it's usually not how people write code IRL and it often confuses people.
For a long time, the default choice in the ecosystem to do this "lowering" operation was MonadBaseControl, and this had a bunch of downsides: hella confusing types, difficult to implement instances, impossible to derive instances, sometimes confusing behavior. Not a great situation.
MonadUnliftIO restricts things to a simpler set of monad transformers, and is able to provide relatively simple types, derivable instances, and always predictable behavior. The cost is that ExceptT, StateT, etc transformers can't use it.
The underlying principle is: by restricting what is possible, we make it easier to understand what might happen. MonadBaseControl is extremely powerful and general, and quite difficult to use and confusing as a result. MonadUnliftIO is less powerful and general, but it's much easier to use.
So this appears to be saying that state is not mutated in the monad m when using askUnliftIO.
This isn't true - the law is stating that unliftIO shouldn't do anything with the monad transformer aside from lowering it into IO. Here's something that breaks that law:
newtype WithInt a = WithInt (ReaderT Int IO a)
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Int)
instance MonadUnliftIO WithInt where
askUnliftIO = pure (UnliftIO (\(WithInt readerAction) -> runReaderT 0 readerAction))
Let's verify that this breaks the law given: askUnliftIO >>= (\u -> liftIO (unliftIO u m)) = m.
test :: WithInt Int
test = do
int <- ask
print int
pure int
checkLaw :: WithInt ()
checkLaw = do
first <- test
second <- askUnliftIO >>= (\u -> liftIO (unliftIO u test))
when (first /= second) $
putStrLn "Law violation!!!"
The value returned by test and the askUnliftIO ... lowering/lifting are different, so the law is broken. Furthermore, the observed effects are different, which isn't great either.

Can IO action in negative position give unexpected results?

There seems to be some undocumented knowledge about the difference between Monad IO and IO. Remarks here and here) hint that IO a can be used in negative position but may have unintended consequences:
Citing Snoyman 1:
However, we know that some control flows (such as exception handling)
are not being used, since they are not compatible with MonadIO.
(Reason: MonadIO requires that the IO be in positive, not negative,
position.) This lets us know, for example, that foo is safe to use in
a continuation-based monad like ContT or Conduit.
And Kmett 2:
I tend to export functions with a MonadIO constraint... whenever it
doesn't have to take an IO-like action in negative position (as an
argument).
When my code does have to take another monadic action as an argument,
then I usually have to stop and think about it.
Is there danger in such functions that programmers should know about?
Does it for example mean that running arbitrary continuation-based action may redefine control flow giving unexpected results in ways that Monad IO based interface are safe from?
Is there danger in such functions that programmers should know about?
There is not danger. Quite the opposite, the point Snoyman and Kmett are making is that Monad IO doesn't let you lift through things with IO in a negative positive.
Suppose you want to generalize putStrLn :: String -> IO (). You can, because the IO is in a positive position:
putStrLn' :: MonadIO m => String -> m ()
putStrLn' str = liftIO (putStrLn str)
Now, suppose you want to generalize handle :: Exception e => (e -> IO a) -> IO a -> IO a. You can't (at least not with just MonadIO):
handle' :: (MonadIO m, Exception e) => (e -> m a) -> m a -> m a
handle' handler act = liftIO (handle (handler . unliftIO) (unliftIO act))
unliftIO :: MonadIO m => m a -> IO a
unliftIO = error "MonadIO isn't powerful enough to make this implementable!"
You need something more. If you're curious about how you'd do that, take a look at the implementation of functions in lifted-base. For instance: handle :: (MonadBaseControl IO m, Exception e) => (e -> m a) -> m a -> m a.

How to create a limited version of the IO monad

I have written a number of functions using a monad transformer stack:
data Options
data Result
data Input
type Ingest a = EitherT String (ReaderT Options IO) a
foo :: Input -> Ingest Result
and so on. Now, most of these functions are fundamentally pure. I only need IO in one of these functions: this function reads a file, and logs (using log :: String -> IO ()) that it has done so. So the impurity of this one function "infects" my whole codebase, making all these functions capable of doing IO even though they don't need to, except to call this one function. This is distasteful for two reasons:
It doesn't make it clear what limited subset of IO these might perform
It doesn't make it clear which functions actually perform that IO
A colleague suggested parameterizing Ingest over a base monad type, which I like. Specifically, define a typeclass for reading the contents of a file, and provide an instance for IO as well as perhaps for some other monad to use in writing tests:
class Monad m => ReadFile m where
readFile :: FilePath -> m Text
instance ReadFile IO where
readFile = Data.Text.IO.readFile
A typeclass for logging already exists, so I could just use that existing typeclass. But then I'm not sure how to use this new class. Firstly, what do I replace my type alias with? I can't write
type Ingest m a = (Logging m, ReadFile m) => EitherT String (ReaderT Options m) a
because constraints are not permitted on type synonyms. Must I add that constraint to each of my functions? In principle this is nice because it flags the functions which may need to read a file, but in practice these functions are mutually recursive, and so all of them need that permission, making it a pain to write them all out.
I could define a newtype wrapper instead of a type synonym, but I don't think that makes things any better: I still have to add this new constraint to each of my functions.
Secondly, how do I actually call the functions of my new typeclass, from within the EitherT/ReaderT stack? I can't simply write
foo :: ReadFile m => FilePath -> Ingest m Text
foo = readFile
because this ignores the EitherT and ReaderT wrappers. Do I instead write this?
foo :: ReadFile m => FilePath -> Ingest m Text
foo = lift . lift $ readFile
Seems kinda a pain and quite fragile to the structure of the transformer stack. Do I write a number of instances like
instance ReadFile m => ReadFile (EitherT e m) where
readFile = lift readFile
? That seems like a frustrating amount of boilerplate as well.
Instead of using IO directly, wrap it in a abstract newtype e.g:
module IOLog(IOLog, logMsg) where
newtype IOLog a = IOLog (IO a)
instance Functor IOLog where ...
instance Monad IOLog where ...
logMsg :: String -> IOLog ()
logMsg = logIO . log
-- local definitions --
--
logIO :: IO a -> IOLog a
logIO = IOLog
log :: String -> IO ()
.
.
.
and use that to define Ingest:
type Ingest a = EitherT String (ReaderT Options IOLog) a
In this way you can control what subset of I/O the rest of the program can use, for the price of an extra module - no type-system extensions needed!

Type inference seems like a magic

I have following code snippet and could not configure it out, how it works:
embedded :: MaybeT (ExceptT String (ReaderT () IO)) Int
embedded = return 1
How it is possible to give only a number and get such as type signature back? How does the compiler do that?
The choice of wording is a bit unfortunate. It's not the case that the expression return 1 gives back the type signature MaybeT (ExceptT String (ReaderT () IO)) Int.
As n.m. writes in the comments, if you don't supply a type, the expression is much more general:
Prelude> embedded = return 1
Prelude> :type embedded
embedded :: (Num a, Monad m) => m a
By annotating with a type, you explicitly state that you want something less general than that.
Specifically, you state that you want the type MaybeT (ExceptT String (ReaderT () IO)) Int.
How does return work? MaybeT m a is a Monad when m is a Monad, and return is defined like this:
return = lift . return
The right-hand return is the return function that belongs to the 'inner' Monad, whereas lift is defined by MonadTrans and lifts the underlying monadic value up to MaybeT.
That explains how a MaybeT value is created, but isn't the whole story.
In this case, the 'inner' Monad is ExceptT String (ReaderT () IO), which is another Monad (in fact, another MonadTrans). return is defined like this:
return a = ExceptT $ return (Right a)
Notice that this is another nested return, where the right-hand return belongs to yet another nested Monad.
In this case, the nested Monad is ReaderT () IO - another MonadTrans. It defines return like this:
return = lift . return
Yet another nested return, where the right-hand return is the return defined for IO (in this particular case).
All of this is parametrised with a, which in this case you've constrained to Int.
So return 1 first takes the pure value 1 and packages it in IO Int. This then gets lifted to ReaderT () IO Int, which again gets packaged into an ExceptT String (ReaderT () IO) Int. Finally, this values gets lifted to MaybeT.

Haskell -- dual personality IO / ST monad?

I have some code that currently uses a ST monad for evaluation. I like not putting IO everywhere because the runST method produces a pure result, and indicates that such result is safe to call (versus unsafePerformIO). However, as some of my code has gotten longer, I do want to put debugging print statements in.
Is there any class that provides a dual-personality monad [or typeclass machinery], one which can be a ST or an IO (depending on its type or a "isDebug" flag)? I recall SPJ introduced a "Mutation" class in his "Fun with Type Functions" paper, which used associative types to relate IO to IORef and ST to STRef. Does such exist as a package somewhere?
Edit / solution
Thanks very much [the nth time], C.A. McCann! Using that solution, I was able to introduce an additional class for monads which support a pdebug function. The ST monad will ignore these calls, whereas IO will run putStrLn.
class DebugMonad m where
pdebug :: String -> m ()
instance DebugMonad (ST s) where
pdebug _ = return ()
instance DebugMonad IO where
pdebug = putStrLn
test initV = do
v <- newRef initV
modifyRef v (+1)
pdebug "debug"
readRef v
testR v = runST $ test v
This has a very fortunate consequence in ghci. Since it expects expressions to be IO types by default, running something like "test 3" will result in the IO monad being run, so you can debug it easily, and then call it with something like "testR" when you actually want to run it.
If you want a unified interface to IORef and STRef, have you looked at the stateref package? It has type classes for "references to mutable data", separated for readable, writable, etc., with instances for IORef and STRef, as well as things like TVar, MVar, ForeignPtr, etc.
Have you considered Debug.Trace.trace instead?
http://www.haskell.org/haskellwiki/Debugging

Resources