Introduction
When you write your first Haskell program, immediately you would come across the type IO a
, called “the IO monad”.
|
|
If you go lookup what the definition of a Monad
is, you would see it is a subtype of Applicative
, and Applicative
is a subtype of Functor
. Very overwhelming, right?
Let’s start with a simple definition and refine it as we go along. A Monad a
is simply a value that has something to do with a
but not quite an a
by itself (recall that in Haskell, every value is pure).
For example it may be:
- a value that is of type
a
but is wrapped in a context, maybe some states or some side effects. (the above “Hello, World!” program is an example of this) - a value
a
but is entered by the user at runtime.
The Monad Bakery
Now let’s look at the IO monad. Suppose we are a baker at a bakery. baking is a complicated process and cannot be modeled as a pure function. So we would have something like this:
|
|
What I wanted to express is, IO Bread
is simply a “recipe” for Bread
: it is not by itself a Bread
, but it is a process that can produce a Bread
.
Functors
A Functor
is a typeclass that maps values of one type to another type (note Haskell use the mathematical definition of a functor, it’s different from the OOP functor).
Now let’s look at why a Monad
is a subtype of Functor
. After you bake a bread, you may want to display it to the customer on a plate, or pack it in a bag to sell. Let’s say you have these functions:
|
|
Note that these take the Bread
as a value but we only have a recipe for Bread
! However we can turn a recipe for Bread
into a recipe for Plate Bread
or Bag Bread
:
|
|
Now we have a recipe for Plate Bread
and Bag Bread
respectively. However, being a Functor
, we can simplify the code by using the fmap
function provided by the Functor
typeclass:
|
|
The infix operator <$>
is just a synonym for fmap
, so the above code can be written as:
|
|
There are a few helper in the Functor
typeclass that are useful for composing functors:
(<$)
is defined asf <$ a = fmap (const f) a
, which replaces the value inside the functor with a constant value. The flipped version($>)
is also provided.void
is defined asvoid a = () <$ a
, which replaces the value inside the functor with()
.
Applicatives
An Applicative
is a typeclass that allows you to apply a function that is wrapped in a context to a value that is also wrapped in a context. It is a concept that is more general than Functor
. It took me quite a while to understand this when I first learned Haskell, so let’s look at the mixIngredients
function again:
|
|
How do I turn a recipe for Flour
, Water
, and Yeast
into a recipe for Dough
?
Using the do
notation, we can write:
|
|
However this is just syntactic sugar, under the hood it is just a composition of functions by using the <*>
operator provided by the Applicative
typeclass:
|
|
Elegant, isn’t it? How does this work?
Recall two facts:
- All Haskell functions are curried, so applying an
a
to ana -> b -> c
function gives you ab -> c
function. (->)
is right-associative, soa -> b -> c
is a subtype ofa -> d
whered
is bound to beb -> c
.
So the type is inferred as:
|
|
The first step uses fmap
(a.k.a. (<$>)
) to make a Flour -> Water -> Yeast -> Dough
function into a IO (Water -> Yeast -> Dough)
function, then the <*>
operator applies the Water
and Yeast
sequentially to get a IO Dough
.
There are a few helper functions in the Applicative
typeclass that are useful for composing applicatives:
pure
is defined by the implementation of theApplicative
typeclass, and it is used to lift a value into a context, a.k.a.a -> f a
.(<**>)
, which is a flipped version of<*>
.'liftA2
is defined asliftA2 f a b = f <$> a <*> b
, which applies a binary function to two values in a context, similarlyliftA3
is also provided.(<*)
and(*>)
does two things in sequence but only returns the result of the first or second computation respectively.
Monads
A Monad
is simply an Applicative
that allows you to “bind” the result of a computation to a function that returns a computation. Now that you have got your recipe for a Dough
and a recipe for baking a Dough
into a Bread
, how can you get a recipe for Bread
? You bind the result of the first recipe to the second recipe!
|
|
Putting it all together
Going back to the initial example, now we see that the do
notation is just a syntactic sugar to make it look like you are writing imperative code, and the unsugared version is:
|
|
Note the operator precedence: <$>
and <*>
have higher precedence than >>=
, so the above code is equivalent to:
|
|
Now we see that everything do
notation does can be done purely functionally, they are just syntactic sugar for composing functions in these ways:
- Mapping the result of a computation to another type:
fmap
or<$>
- Apply the result of a computation to a function partially applied to the result of another computation:
<*>
- Binding the result of a computation to another computation:
>>=
Note that I’m not saying do
notation is bad, it is a very useful tool for writing code that involve a lot of composing monadic values in a natural and readable way. However, for simple tasks like the above, it is better to use the unsugared version, which is more terse and faster to understand for a peer Haskell programmer.
However, use do
notation generously if you expect someone who is not familiar with Haskell to read your code.
The commonly-used helper functions in the Monad
typeclass are (some of them are only available in the Control.Monad
module, not the prelude):
return
is justpure
but only forMonad
(I recommend writingpure
instead ofreturn
unless it is the last statement in ado
notation, as it is more general and distinguishable from thereturn
in other languages).sequence
converts a list of monadic values into a monadic list of values. This is used to convert a list of computations to a single computation that returns a list of values.sequence_
is similar tosequence
but discards the result.
replicateM
repeats a computation a number of times andsequence
the results.replicateM_
is similar toreplicateM
but discards the result.
forever
repeats a computation forever.when
andunless
conditionally execute a computation, basically shortcut ofif cond then action else pure ()
.mapM
takes a function that returns a monadic value and applies it to a list of values, thensequence
the results.mapM_
is similar tomapM
but discards the result.
forM
is similar tomapM
but with the arguments flipped, similarlyforM_
ismapM_
with the arguments flipped.foldM
is similar tofoldl
but the folding function rturns a monadic value.(>>)
is identical to(*>)
, but only available in theMonad
typeclass.
Generalization of Monads
Now that we have seen how IO
can be modeled as a Monad
, let’s see what else can be modeled as a Monad
. s
Parser Combinators
A common task suitable for Haskell is parsing. Parser combinators are a way to build a parser by combining smaller parsers. We will use the built-in Text.ParserCombinators.ReadP
module as an example:
Let’s say you just bought a new recipe book and you want to parse the recipe for a bread. The recipe is in the format of:
|
|
We can define a parser for the recipe:
|
|
This prints:
|
|
Note that these ReadP a
’s are technically values in Haskell but they represent a computation that either produces a value of type a
or fails, given the context of the input string. This is why ReadP
is a Monad
.
State
Another common task suitable for Haskell is stateful computation. We will use the built-in Control.Monad.State
module as an example:
Let’s say you want to keep track of the amount of flour, water, and yeast you have in your bakery and write a recipe that automatically buys more ingredients when you run out. You can define a stateful computation to keep track of the inventory:
|
|
Monad Transformers
Here comes a problem: what if I need to bake a bread and I need both the IO
and the State
monad? You can use a monad transformer to combine two monads into one. For example, you can use StateT
to combine State
and IO
, putting it all together:
|
|
Now when we invoke bakeBread
, we can be sure that we have enough ingredients to bake the bread!
|
|
Some more type wizardry: if you suddenly decide some functions only need to check the inventory but not use IO, you can use the Identity
monad, which is a monad that does nothing but store a value:
|
|
When you want to compose this into any other BakeryT m
monad, you can use this uplift
function:
|
|
runState
returns the new state and the value, (pure .)
lifts the value into the m
monad, and StateT
lifts the function into the StateT
monad transformer, neat!
Extras
Shortcuts to IO monad
Sometimes you don’t really care about how an IO value is composed into your big main :: IO ()
, and you just want to use the result lazily as a value. You can use the unsafePerformIO
function to do this, but be careful as it can break referential transparency, as there is no guarantee that when the value is used you get a memoized value back or a new computation is done.
Note that it is not a very good idea to use unsafePerformIO
as a kind of “lazy global variable” in your program, as it may be ran multiple times, instead compute it first and compose it with the rest of your program.
However one use case I use a lot in my study is to use it to emit intermediate steps in a computation, for example:
|
|
Recall that seq
is a function that forces the evaluation of the first argument before returning the second argument, when the second argument is needed.
Here, we get a function that, when a
is evaluated, writes out an intermediate step to a file handle. This is very useful when you are writing lazy algorithms and you want to see the intermediate steps that only involve computations that are actually done, and in order of when they are needed! In short, it produces a step-by-step trace of the computation that looks like a human would write it.
Let’s see it in action! (note I changed hEmitStep
a bit by seq
ing the result before putting it to hPutStrLn
to avoid the -->
from being printed before the actual value is computed (which may be a problem if the actual value itself prints more intermediate steps)):
|
|
This produces:
|
|
Nice! I only said to emit step when this value is needed, I never need to explicitly say when to emit the step, lazy functional programming does it for me! Additionally, if you use the values multiple times, it won’t spam the output with the same step, as the value is memoized (in a reasonable way of course).
The same code would be very annoying to write in an imperative language, as you would need to explicitly write out the steps and the conditions to emit the steps, not to mention simulating infinite lists with iterators or callbacks. Additionally, an imperative implementation that does not involve emitting steps would look very different from the one that does, inside out. However here we only need to delete the emitStep
calls and the program still works, just like you would write it without emitting steps: if you ask ChatGPT to write a prime program, it would look exactly like this, but without the emitStep
calls!
|
|
If you ask it to do it in Python, you get this:
|
|
It is really hard to make this code emit the correct steps, you might think that you can just add a print()
after D.setdefault()
, but no, you emit numbers before the previous prime numbers are emitted. This would print:
|
|
Conclusion
In general, Haskell models non-pure computations as compositions of monads, and the program entry point is modeled as a single non-pure monad IO ()
. This way, everything that seems to be impure in Haskell code is simply transforming small impure computations into larger impure computations, in a pure way.