Making the Jump II: Using More Monads
A few weeks ago, we addressed some important steps to advance past the "beginner" stage of Haskell. We learned how to organize your project and how to find the relevant documentation. This week we’re going to continue to look at another place where we can make a big step up. We’ll explore how to expand our vocabulary on monad usage.
Monads are a vital component of Haskell. You can’t use a lot of libraries unless you know how to incorporate their monadic functions. These functions often involve a monad that is custom to that library. When you’re first starting out, it can be hard to know how to incorporate these monads into the rest of your program.
In this article, we’ll focus on a specific pattern a lot of monads and libraries use. I call this pattern the “run” pattern. Often, you’ll use a function with a name like runXXX
or runXXXT
, where XXX
is the name of the monad. These functions will always take a monadic expression as their first argument. Then they'll also take some other initialization information, and finally return some output. This output can either be in pure form or a different monad you’re already using like IO
. We’ll start by seeing how this works with the State
monad, and then move onto some other libraries.
Once you grasp this topic, it seems very simple. But a lot of us first learned monads with a bad mental model. For instance, the first thing I learned about monads was that they had side effects. And thus, you can only call them from places that have the same side effects. This applies to IO but doesn’t generalize to other monads. So even though it seems obvious now, I struggled to learn this idea at first. But let's start looking at some examples of this pattern.
For a more in depth look at monads, check out our series on Functional Data Structures! We start by learning about simpler things like functors. Then we eventually work our way up to monads and even monad transformers!
The Basics of “Run”: The State Monad
Let’s start by recalling the State
monad. This monad has a single type parameter, and we can access this type as a global read/write state. Here’s an example function written in the State
monad:
stateExample :: State Int (Int, Int, Int)
stateExample = do
a <- get
modify (+1)
b <- get
put 5
c <- get
return (a, b, c)
If this function is confusing, you should take a look at the documentation for State
. It’ll at least show you the relevant type signatures. First we read the initial state. Then we modify it with some function. Finally we completely change it.
In the example above, if our initial state is 1, we’ll return (1,2,5)
as the result. If the initial state is 2, we’ll return (2,3,5)
. But suppose we have a pure function. How do we call our state function?
pureFunction :: Int -> Int
pureFunction = ???
The answer is the runState
function. We can check the documentation and find its type:
runState :: State s a -> s -> (a, s)
This function has two parameters. The first is a State
action. We’ll pass our function above as this parameter! Then the second is the initial state, and this is how we’ll configure it. Then the result is pure. It contains our result, as well as the final value of the state. So here’s a sample call we can make that gives us this monadic expression in our pure function. We’ll call it from a where
clause, and discard the final state:
pureFunction :: Int -> Int
pureFunction input = a + b + c
where
((a,b,c), _) = runState stateExample input
This is the simplest example of how we can use the runXXX
pattern.
Upgrading to Transformers
Now, suppose our State
function isn’t quite pure. It now wants to print some of its output, so it’ll need the IO
monad. This means it’ll use the StateT
monad transformer over IO
:
stateTExample :: StateT Int IO (Int, Int, Int)
stateTExample = do
a <- get
lift $ print “Initial Value:”
lift $ print a
modify (+1)
b <- get
lift $ putStrLn “After adding 1:”
lift $ print b
put 5
c <- get
lift $ putStrLn “After setting as 5:”
lift $ print c
return (a, b, c)
Now instead of calling this function from a pure format, we’ll need to call it from an IO
function. But once again, we’ll use a runXXX
function. Now though, since we’re using a monad transformer, we won’t get a pure result. Instead, we’ll get our result in the underlying monad. This means we can call this function from IO
. So let’s examine the type of the runStateT
function. We’ve substituted IO
for the generic monad parameter m
:
runStateT :: StateT s IO a -> s -> IO (a, s)
It looks a lot like runState
, except for the extra IO
parameters! Instead of returning a pure tuple for the result, it returns an IO
action containing that result. Thus we can call it from the IO
monad.
main :: IO ()
main = do
putStrLn “Please enter a number.”
input <- read <$> getLine
results <- runStateT stateTExample input
print results
We’ll get the following output as a result:
Please enter a number.
10
Initial Value:
10
After adding 1
11
After setting as 5
5
(10, 11, 5)
Using Run
For Libraries
This pattern will often extend into libraries you use. For example, in our series on parsing, we examine the Megaparsec library. A lot of the individual parser combinators in that library exist in the Parsec
or ParsecT
monad. So we can combine a bunch of different parsers together into one function.
But then to run that function from your normal IO
code (or another monad), you need to use the runParserT
function. Let’s look at its type signature:
runParserT
:: Monad m
-> ParsecT e s m a
-> String -- Name of source file
-> s -- Input for parser
-> m (Either (ParseError (Token s) e) a)
There are a lot of type parameters there that you don’t need to understand. But the structure is the same. The first parameter to our run
function is the monadic action. Then we’ll supply some other inputs we need. Then we get some result, wrapped in an outer monad (such as IO
).
We can see the same pattern if we use the servant-client
library to make client-side API calls. Any call you make to your API will be in the ClientM
monad. Now here’s the type signature of the runClientM
function:
runClientM :: ClientM a -> ClientEnv -> IO (Either ServantError a)
So again, the same pattern emerges. We’ll compose our monadic action and pass that as the first parameter. Then we’ll provide some initial state, in this case a ClientEnv
. Finally, we’ll get our result (Either ServantError a
) wrapped in an outer monad (IO
).
Monads Within Expressions
It’s also important to remember that a lot of basic monads work without even needing a runXXX
function! For instance, you can use a Maybe
or Either
monad to take out some of your error handling logic:
divideIfEven :: Int -> Maybe Int
divideIfEven x = if x `mod` 2 == 0
then Just (x `quot` 2)
else Nothing
dividesBy8 :: Int -> Bool
dividesBy8 = case thirdResult of
Just _ -> True
Nothing -> False
where
thirdResult :: Maybe Int
thirdResult = do
y <- divideIfEven x
z <- divideIfEven y
divideIfEven z
Conclusion
Monads are the key to using a lot of different Haskell libraries. But when you’re first starting out, it can be very confusing how you call into these functions from your code. The same applies with some common monad transformers like Reader
and State
. The most common pattern to look out for is the runXXXT
pattern. Master this pattern and you’re well on your to understanding monads and writing better Haskell!
For a closer look at monads and similar structures, make sure to read our series on Functional Data Structures. If the code in this article was confusing, you should definitely check it out! And if you’ve never written Haskell but want to start, download our Beginners Checklist!