Last week we looked at how monads can help you make the next jump in your Haskell development. We went over the
runXXXT pattern and how it’s a common gateway for us to use certain monads from the rest of our code. But sometimes it also helps to go back to the basics. I actually went a long time without really grasping how to use a couple basic monads. Or at the very least, I didn’t understand how to use them as monads.
In this article, we’ll look at how to use the list monad and the function monad. Lists and functions are core concepts that any Haskeller learns from the get-go. But the list data structure and function application are also monads! And understanding how they work as such can teach us more about how monads work.
For an in-depth discussion of monads, check out our Functional Data Structures Series!
The General Pattern of Do Syntax
do syntax is one of the keys to understanding how to actually use monads. The bind operator makes it hard to track where your arguments are. Do syntax keeps the structure clean and allows you to pass results with ease. Let’s see how this works with
IO, the first monad a lot of Haskellers learn. Here’s an example where we read the second line from a file:
readLineFromFile :: IO String readLineFromFile = do handle <- openFile “myFile.txt” ReadMode nextLine <- hGetLine handle secondLine <- hGetLine handle _ <- hClose handle return secondLine
By keeping in mind the type signatures of all the
IO functions, we can start to see the general pattern of do syntax. Let’s replace each expression with its type:
openFile :: FilePath -> IOMode -> IO Handle hGetLine :: Handle -> IO String hClose :: Handle -> IO () return :: a -> IO a readLineFromFile :: IO String readLineFromFile = do (Handle) <- (IO Handle) (String) <- (IO String) (String) <- (IO String) () <- (IO ()) IO String
Every line in a do expression (except the last) uses the assignment operator
<-. Then it has an expression of
IO a on the right side, which it assigns to a value of
a on the left side. The last line’s type then matches the final return value of this function. What’s important now is to recognize that we can generalize this structure to ANY monad:
monadicFunction :: m c monadicFunction = do (_ :: a) <- (_ :: m a) (_ :: b) <- (_ :: m b) (_ :: m c)
So for example, if we have a function in the
Maybe monad, we can use it and plug that in for
myMaybeFunction :: a -> Maybe a monadicMaybe :: a -> Maybe a monadicMaybe x = do (y :: a) <- myMaybeFunction x (z :: a) <- myMaybeFunction y (Just z :: Maybe a)
The important thing to remember is that a monad captures a computational context. For
IO, this context is that the computation might interact with the terminal or network. For
Maybe, the context is that the computation might fail.
The List Monad
Now to graph the list monad, we need to know its computational context. We can view any function returning a list as non-deterministic. It could have many different values. So if we chain these computations, our final result is every possible combination. That is, our first computation could return a list of values. Then we want to check what we get with each of these different results as an input to the next function. And then we’ll take all those results. And so on.
To see this, let’s imagine we have a game. We can start that game with a particular number
x. On each turn, we can either subtract one, add one, or keep the number the same. We want to know all the possible results after 5 turns, and the distribution of the possibilities. So we start by writing our non-deterministic function. It takes a single input and returns the possible game outputs:
runTurn :: Int -> [Int] runTurn x = [x - 1, x, x + 1]
Here’s how we’d apply on this 5 turn game. We’ll add the type signatures so you can see the monadic structure:
runGame :: Int -> [Int] runGame x = do (m1 :: Int) <- (runTurn x :: [Int]) (m2 :: Int) <- (runTurn m1 :: [Int]) (m3 :: Int) <- (runTurn m2 :: [Int]) (m4 :: Int) <- (runTurn m3 :: [Int]) (m5 :: Int) <- (runTurn m4 :: [Int]) return m5
On the right side, every expression has type
[Int]. Then on the left side, we get our
Int out. So each of the
m expressions represents one of the many solutions we'll get from
runTurn. Then we run the rest of the function imagining we’re only using one of them. In reality though, we’ll run them all, because of how the list monad defines its bind operator. This mental jump is a little tricky. And it’s often more intuitive to just stick to using
where expressions when we do list computations. But it's cool to see patterns like this pop up in unexpected places.
The Function Monad
The function monad is another one I struggled to understand for a while. In some ways, it's the same as the
Reader monad. It encapsulates the context of having a single argument we can pass to different functions. But it’s not defined in the same way as
Reader. When I tried to grok the definition, it didn’t make much sense to me:
instance Monad ((->) r) where return x = \_ -> x h >>= f = \w -> f (h w) w
return definition makes sense. We’ll have a function that takes some argument, ignore that argument, and give the value as an output. The bind operator is a little more complicated. When we bind two functions together, we’ll get a new function that takes some argument
w. We’ll apply that argument against our first function (
(h w)). Then we’ll take the result of that, apply it to
f, and THEN also apply the argument
w again. It’s a little hard to follow.
But let’s think about this in the context of do syntax. Every expression on the right side will be a function that takes our type as its only argument.
myFunctionMonad :: a -> (x, y, z) myFunctionMonad = do x <- :: a -> b y <- :: a -> c z <- :: a -> d return (x, y, z)
Now let’s imagine we’ll pass an
Int and use a few different functions that can take an
Int. Here’s what we’ll get:
myFunctionMonad :: Int -> (Int, Int, String) myFunctionMonad = do x <- (1 +) y <- (2 *) z <- show return (x, y, z)
And now we have valid do syntax! So what happens when we run this function? We’ll call our different functions on the same input.
>> myFunctionMonad 3 (4, 6, "3") >> myFunctionMonad (-1) (0, -2, "-1")
When we pass 3 in the first example, we add 1 to it on the first line, multiply it by 2 on the second line, and
show it on the third line. And we do this all without explicitly stating the argument! The tricky part is that all your functions have to take the input argument as their last argument. So you might have to do a little bit of argument flipping.
In this article we explored lists and functions, two of the most common concepts in Haskell. We generally don’t use these as monads. But we saw how they still fit into the monadic structure. We can use them in do-syntax, and follow the patterns we already know to make things work.
Perhaps you’ve tried to learn Haskell before but found monads a little too complex. Hopefully this article helped clarify the structure of monads. If you want to get your Haskell journey back under way, download our Beginners Checklist! Or to learn monads from the ground up, read our series on Functional Data Structures!