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
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
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
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
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
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
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)
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
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
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 :: 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 (
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
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
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
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!