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
abut is wrapped in a context, maybe some states or some side effects. (the above “Hello, World!” program is an example of this) - a value
abut 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.voidis 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
ato ana -> b -> cfunction gives you ab -> cfunction. (->)is right-associative, soa -> b -> cis a subtype ofa -> dwheredis 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:
pureis defined by the implementation of theApplicativetypeclass, and it is used to lift a value into a context, a.k.a.a -> f a.(<**>), which is a flipped version of<*>.'liftA2is defined asliftA2 f a b = f <$> a <*> b, which applies a binary function to two values in a context, similarlyliftA3is 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:
fmapor<$> - 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):
returnis justpurebut only forMonad(I recommend writingpureinstead ofreturnunless it is the last statement in adonotation, as it is more general and distinguishable from thereturnin other languages).sequenceconverts 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 tosequencebut discards the result.
replicateMrepeats a computation a number of times andsequencethe results.replicateM_is similar toreplicateMbut discards the result.
foreverrepeats a computation forever.whenandunlessconditionally execute a computation, basically shortcut ofif cond then action else pure ().mapMtakes a function that returns a monadic value and applies it to a list of values, thensequencethe results.mapM_is similar tomapMbut discards the result.
forMis similar tomapMbut with the arguments flipped, similarlyforM_ismapM_with the arguments flipped.foldMis similar tofoldlbut the folding function rturns a monadic value.(>>)is identical to(*>), but only available in theMonadtypeclass.
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 seqing 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.