In this part of the course, we observe how we can abstract from certain recurring patterns that arise in larger Haskell developments and thereby discover monads (and applicative functors) as useful interfaces. The advantage of having these interfaces explicit in the form of type classes is that we can build substantial libraries just in terms of these interfaces. This, in turn, means that once we have identified a particular type as a monad, we get a large amount of functionality for free.
We start by discussing the Maybe type, and then move on to a new type that is
called State. We later discuss other types as well, and also revisit IO.
Accompanying materials
6–1 Preparation: Finite Maps
As a starting point for our discussion of Maybe, we return to our example of
key-value tables. But this time, we use proper efficient tables as implemented
by the containers package.
Self Test
Assuming that
Data.Map.Strictis imported qualified asM, what is the result ofM.lookup 1 (M.insert 1 'x' (M.insert 1 'y' M.empty))?What is the result of
M.lookup 1 (M.insert 2 undefined M.empty)? Does the result change if you importData.Map.Lazyinstead ofData.Map.Strict?The
Maptype fromData.Map.StrictandData.Map.Lazyis an instance of theFunctorandFoldableclasses. Try some invocations. Isfmap-ping over a finite map affecting the keys or the values? What doeslengthon a finite map compute? What doessumcompute?
6–2 Sequencing Possibly Failing Computations
We consider an example that involves running several possibly failing computations
in sequence, leading to a rather noisy nested case expression that we are then
trying to abstract from. As a result of this abstraction process, we discover a
function with a similar type as (>>=) had in the context of IO.
Self Test
Given the definitions of
(>>>=)from the video, what is the result of the following invocations?Nothing >>>= Just Just 3 >>>= Just Just 3 >>>= Just . (+1) Just (Just 3) >>>= id Just 3 >>>= const NothingDefine a function
someLookups :: Int -> Int -> Maybe IntwheresomeLookups n iis performingnsuccessive lookups in the style of thethreeLookupsfunction, starting withi, also incrementing the final result by1. In particular,someLookups 3should be the same asthreeLookups. Do this with and without the use of(>>>=).
6–3 The Monad Class
We notice that Maybe does not just support a (>>=)-like, but also
a return-like function. Types supporting these two operations are called
monads. We discuss the monad class and see that Maybe is indeed an instance
of this class. We observe that do notation is actually available for all
instances of Monad, so we can use it to further simplify our example. And
all functions that can be defined in terms of just return and bind, functions
such as sequence or mapM or filterM, become available to use on Maybe
computations as well.
Self Test
Import
Control.Monadand predict the results of the following calls:replicateM 7 (Just 2) replicateM 5 Nothing mapM return [1..10] :: Maybe [Int] mapM return [1..] :: Maybe [Int] filterM return [True, False, True] :: Maybe [Bool]Is
mapMincremental? Should it be? Can it be?Define a function
safeDiv :: Integral a => a -> a -> Maybe athat producesNothingrather than an exception on division by0and otherwise behaves likediv; then define a functiondivisions :: Int -> [Int] -> Maybe [Int]that divides the given integer by all the integers in the list, failing if any of the divisions fail.By looking at the type and trying a few example calls on
Maybeinputs, try to figure out what the functionjoindoes. Try e.g.join (Just (Just 3)) join (Just Nothing) join Nothing
6–4 More Functions for Free
We explore the implications of the Monad type class a bit further, by
discussing for a few more functions we have previously discussed only in
the context of IO how they can in fact be generalised to arbitrary
instances of the Monad class, and how they behave specifically in the
Maybe setting.
Self Test
Reproduce the definition of
lookupsfrom the video.Try to define
joinfrom the previous self test just in terms ofreturnand(>>=)(and/ordonotation).Recall the functions
followandsearchfrom Assignments B. Do they benefit from the fact thatMaybeis an instance ofMonad? If yes, rewrite them? If not, why not?Recall the function
mapM_from the Self Test of Video 5–6. IsmapM_ever useful for theMaybetype?
6–5 Recap: Labelling Trees
As another and entirely independent example, we consider a task from Assignments B, that of labelling all the nodes in a binary tree with unique labels. As a preparation, we first solve this task in classic pattern-matching style. We need to introduce an auxiliary function that has an extra integer as an input (representing the label from which we want to start labelling the current tree), and an extra integer as an output (representing the label that is the first label free to use after we are done with labelling the current tree).
Self Test
How do we have to adapt
labelTreeAuxif we want to label the element in the current node before we label the left and the right subtree?Define a variant of
labelTreethat is of typecompleteTree :: BinTree (Maybe Int) -> BinTree Int. It should should fill all the occurrences ofNothingin the tree with unique counter values from left to right.
6–6 Counter
As an intermediate step before we identify a recurring pattern in the
tree-labelling example, we introduce a Counter type to represent
computations that maintain an Int-typed counter in order to come up
with a result. We refactor the definition to make use of this Counter
type and additionally introduce a helper function stepCounter to make
the overall shape of the code more regular.
Self Test
What is the type and result of the following expression? Predict before you try:
runCounter (MkCounter (\ c -> (True, c + 1))) 5Define a version of
stepCounterthat resets the counter to0as soon as it becomes larger than5. How does this change the behaviour of thelabelTreefunction?
6–7 Monad Operations for Counter
We derive a “bind” and a “return” operation for counters and see how
we can rewrite and now drastically simplify the definition of labelTreeAux
by applying these functions, hiding the threading of the counter completely
and thereby removing the potential for making mistakes.
Self Test
What is the result of the following calls:
runCounter (return 0) 5 runCounter (stepCounter >>>>= \ x -> stepCounter >>>>= \ y -> returnCounter (x + y)) 5 runCounter (stepCounter >>>>= \ x -> stepCounter >>>>= \ _ -> returnCounter x) 5Change the type of the
Emptyconstructor so that it also contains an element of typea. Then adaptlabelTreeAuxaccordingly to label these elements as well.
6–8 Functor, Applicative, Monad
In order to actually make Counter and instance of the Monad class,
we have to navigate the Haskell class hierarchy and also provide instance
declarations for the Applicative and Functor classes. Fortunately,
if we have “bind” and “return” both available, all other methods we have
to define can be mechanically completed in terms of just these two
basic ingredients.
Self Test
Define an isomorphic copy of
Maybeas follows:data Option a = None | Some aMake your
Optiontype an instance ofMonadby defining all the necessary instances.The “identity type” can also be made an instance of the
Monadclass. It can be defined as follows:newtype Id a = Id { unId :: a }For this type,
ais isomorphic toId a, and therefore the type ofreturnis isomorphic to the identity function, and the type of “bind”,(>>=) :: Id a -> (a -> Id b) -> Id bis isomorphic to flipped function application. Define a
Monadinstance accordingly. (This type is available under the nameIdentityin the moduleData.Functor.Identity.)
6–9 Optional: Using the Applicative Interface
The definition of labelTreeAux does not actually require the full power
of “bind”. None of the counter-maintaining computations depend on the results
of previous computations. The shape of the tree alone determines the
sequence of computations we want to build. This means that we do not have
to use “bind” or do notation in the definition. We can use operations
provided by the Applicative class. We take a look at a common pattern
sequencing operations with the help of the “applicative star” operator (<*>),
which is resembling function application.
Self Test
Can the
threeLookupsexample be rewritten in applicative style? If so, then do so; if not, argue why not.The function
filterMas defined in theControl.Monadmodule, somewhat ironically, has not aMonad, but only anApplicativeconstraint. Try to provide a definition offilterMusing only the applicative interface.
6–10 Generalising from Counter to State
While Counter is a type that we have defined for the purposes of our examples,
a more general form of Counter actually exists in the libraries, called State.
We show how we can access the general functionality.
Self Test
Define a version of
labelTreethat, as a state, takes a list of labels, i.e.,labelTreeFromSupply :: BinTree a -> [b] -> BinTree (a, b)If the given list is not long enough, the function is exceptionally allowed to crash.
Try to define the accumulator-based
reversefunction usingState. (This is not recommended in general, the pattern offoldl'is too simple to justify usingStatenormally. It just serves as an exercise here.)
6–11 Optional: Other Monads
The monad interface is so compelling because there are, in fact, lots of types
that can be made an instance. However, many types are in fact variants or combinations
of the cases we have now already discussed. We discuss a few such variants.
We also mention lists as an example of a Monad instance that has a rather different
flavour from the examples we have seen so far.
6–12 Assignments F
The final assignments are again distributed as a zip file here.
You can unpack this file into a local directory and then work on the contents
in your editor and using cabal repl. As usual, the source file contains
several skeletons of functions that you are supposed to complete, as well as a
few other questions.
Note that in this public version of the course, there is no way to submit the assignments.
6–13 Conclusion of the Course
We have reached the end of the course. In this short video, I reflect on what we have achieved now and provide some pointers on how you can continue your Haskell journey.