A word on monads
A clever guy once said that once you understand what a monad is you lose the ability to explain what it is to other people - and he's (almost) right. Monads are a weird and wonderful thing, but sometimes are easiest to understand when given an example of one.
I'm going to try and do this by example, so to set the scene - lets use something like the following data structure:
data Person = Person {
name :: String,
mother :: Maybe Person,
father :: Maybe Person
}
So it's super simple. A Person has a name, and optionally has a mother and/or father, specified by the type Maybe Person for each.
Ok so lets get some test data:
gemma = Person {
name = "Gemma",
mother = Just Person {
name = "Michelle",
mother = Just Person {
name = "Sandy",
mother = Nothing,
father = Nothing
},
father = Just Person {
name = "Tim",
mother = Nothing,
father = Nothing
}
},
father = Just Person {
name = "Tom",
mother = Just Person {
name = "Kate",
mother = Nothing,
father = Nothing
},
father = Just Person {
name = "Bob",
mother = Nothing,
father = Nothing
}
}
}
Sorry that uber terse, but it's simply:
. Sandy
/
Michelle <
/ \
Gemma < Kate Tim
\ /
Tom <
\
Bob
Next lets write a function which finds the the maternal grandparent (i.e. the mother of the mother):
maternalgrandmother :: Person -> Maybe Person
maternalgrandmother p =
case mother p of
Nothing -> Nothing
Just mom ->
case mother mom of
Nothing -> Nothing
Just granny -> Just granny
The above is the simplest implementation, and simply solves the question by asking the following questions:
1. Does the person have a mother?
2a. If not, then return Nothing.
2b. If so, then does the mother have a mother?
3a. If not, then return Nothing.
3b. If so, then return that person (the granny).
And of course this would go on and on if we were looking for the grandgrandmother, and grandgrandgrandmother etc. So this is the sort of repetition where a monad might be useful. So in the above example, let me highlight the crap that we'd like to abstract out:
maternalgrandmother :: Person -> Maybe Person
maternalgrandmother p =
case mother p of
Nothing -> Nothing
Just mom ->
case mother mom of
Nothing -> Nothing
Just granny -> Just granny
I.e. we just want the mother of the passed in person, and then the mother of that person - any interim value that happens to be Nothing should return Nothing back to the caller. Simple. So at best we want something like this:
maternalgrandmother :: Person -> Maybe Person
maternalgrandmother p = mother $ mother p
-- with the caveat that it should return Nothing
-- when either 'p' is Nothing, or 'mother p' is
-- Nothing. 'mother mother p' may be Nothing too
-- but thats ok, because we return a Maybe Person
-- so all good there
So this is basically our ideal implementation, but we're missing the mechanism to implement the 'return Nothing when interim results are Nothing' bit. It's captured nicely in the comment, but the compiler unfortunately isn't that clever. So this is where MONADS come in!
The best sorta one-liner explanation of MONADS I could give is that they attempt to abstract the mechanism of computation of expressions in a particular context. I.e. in imperative languages, statements and expressions are computed in order, top to bottom. Obviously you have flow-control constructs to guide the computation in one way or another, but implictly the program will execute the statement on the next line once it's done with the current statement. This is a given.
HOWEVER - in a functional context (i.e. in haskell) - this 'one statement after the next computation strategy' is NOT a given, and infact the decision of HOW to compute, and the ORDER that computation occurs is the responsibility of the current MONAD in play.
So without going into how to actually define your own MONAD, lets rather just look at some existing ones and how they are used. For the example above, the perfect solution is called the MAYBE MONAD (surprise!). And it simply says keep evaluating functions bound into the monad until a function evals to Nothing then return Nothing (stop evalutating subsequent bound functions), otherwise return the evalation of the final bound function.
maternalgrandmother2 :: Person -> Maybe Person
maternalgrandmother2 p =
mother p >>=
(\m -> mother m >>=
(\g -> return (g)))
This is much cleaner! The bind operator (>>=) takes care of all the ugly case handling for us. If either `mother p` or `mother m` returns Nothing, the entire expression returns Nothing. Otherwise, we get our granny wrapped in Just.
But we can make this even cleaner using do notation, which is syntactic sugar for chaining monadic operations:
maternalgrandmother3 :: Person -> Maybe Person
maternalgrandmother3 p = do
m <- mother p
g <- mother m
return g
The do notation makes the code read almost like imperative code, but with all the safety of monadic composition. Each line extracts the value from the Maybe, and if any step fails (returns Nothing), the entire computation short-circuits to Nothing.
Finally, we can also express this using Kleisli composition (>=>), which allows us to compose monadic functions directly:
import Control.Monad ((>=>))
maternalgrandmother4 :: Person -> Maybe Person
maternalgrandmother4 = mother >=> mother
This is the most elegant solution! The Kleisli composition operator (>=>) lets us compose `mother` with itself, creating a function that goes directly from a person to their maternal grandmother. It's beautifully concise and captures the essence of what we're trying to do: apply the mother function twice in sequence.
All three approaches demonstrate the power of monads: they abstract away the boilerplate of checking for Nothing at each step, allowing us to focus on the essential logic of our computation. Whether you prefer the explicit bind operations, the readable do notation, or the elegant Kleisli composition depends on your style and the complexity of your monadic chain.
Article
{ slug: "a-word-on-monads"
, originalUrl: Nothing
, title: "A word on monads"
, date: constructDate 2014 4 15
, keywords: ["haskell","functional programming"]
, cover: { alt: "code", caption: Nothing, src: "/images/covers/code_haskell.png" }
, summary: "No summary yet"
, author: { bio: "Towards an AI-first, hyper-scaled, functional reactive universe!", email: "tom@tomwells.org", name: "Tom Wells" }
, body:
[ FlowParagraph "A clever guy once said that once you understand what a monad is you lose the ability to explain what it is to other people - and he's (almost) right. Monads are a weird and wonderful thing, but sometimes are easiest to understand when given an example of one."
, FlowParagraph "I'm going to try and do this by example, so to set the scene - lets use something like the following data structure:"
, FlowSourceCode { lang: Haskell, body: "\ndata Person = Person {\n name :: String,\n mother :: Maybe Person,\n father :: Maybe Person\n}\n" }
, FlowParagraph "So it's super simple. A Person has a name, and optionally has a mother and/or father, specified by the type Maybe Person for each."
, FlowParagraph "Ok so lets get some test data:"
, FlowSourceCode { lang: Haskell, body: "\ngemma = Person {\n name = \"Gemma\",\n mother = Just Person {\n name = \"Michelle\",\n mother = Just Person { \n name = \"Sandy\", \n mother = Nothing, \n father = Nothing \n },\n father = Just Person { \n name = \"Tim\", \n mother = Nothing, \n father = Nothing \n }\n },\n father = Just Person {\n name = \"Tom\",\n mother = Just Person { \n name = \"Kate\", \n mother = Nothing, \n father = Nothing \n },\n father = Just Person { \n name = \"Bob\", \n mother = Nothing, \n father = Nothing \n }\n }\n}\n" }
, FlowParagraph "Sorry that uber terse, but it's simply:"
, FlowSourceCode { lang: Pseudo, body: "\n. Sandy\n /\n Michelle <\n / \\\nGemma < Kate Tim\n \\ /\n Tom <\n \\\n Bob\n" }
, FlowParagraph "Next lets write a function which finds the the maternal grandparent (i.e. the mother of the mother):"
, FlowSourceCode { lang: Haskell, body: "\nmaternalgrandmother :: Person -> Maybe Person\nmaternalgrandmother p =\n case mother p of\n Nothing -> Nothing\n Just mom ->\n case mother mom of\n Nothing -> Nothing\n Just granny -> Just granny\n" }
, FlowParagraph "The above is the simplest implementation, and simply solves the question by asking the following questions:"
, FlowSourceCode { lang: Pseudo, body: "\n1. Does the person have a mother?\n2a. If not, then return Nothing.\n2b. If so, then does the mother have a mother?\n3a. If not, then return Nothing.\n3b. If so, then return that person (the granny).\n" }
, FlowParagraph "And of course this would go on and on if we were looking for the grandgrandmother, and grandgrandgrandmother etc. So this is the sort of repetition where a monad might be useful. So in the above example, let me highlight the crap that we'd like to abstract out:"
, FlowSourceCode { lang: Haskell, body: "\n maternalgrandmother :: Person -> Maybe Person\n maternalgrandmother p =\n case mother p of\n Nothing -> Nothing\n Just mom ->\n case mother mom of\n Nothing -> Nothing\n Just granny -> Just granny\n" }
, FlowParagraph "I.e. we just want the mother of the passed in person, and then the mother of that person - any interim value that happens to be Nothing should return Nothing back to the caller. Simple. So at best we want something like this:"
, FlowSourceCode { lang: Haskell, body: "\nmaternalgrandmother :: Person -> Maybe Person\nmaternalgrandmother p = mother $ mother p\n -- with the caveat that it should return Nothing\n -- when either 'p' is Nothing, or 'mother p' is\n -- Nothing. 'mother mother p' may be Nothing too\n -- but thats ok, because we return a Maybe Person\n -- so all good there\n" }
, FlowParagraph "So this is basically our ideal implementation, but we're missing the mechanism to implement the 'return Nothing when interim results are Nothing' bit. It's captured nicely in the comment, but the compiler unfortunately isn't that clever. So this is where MONADS come in!"
, FlowParagraph "The best sorta one-liner explanation of MONADS I could give is that they attempt to abstract the mechanism of computation of expressions in a particular context. I.e. in imperative languages, statements and expressions are computed in order, top to bottom. Obviously you have flow-control constructs to guide the computation in one way or another, but implictly the program will execute the statement on the next line once it's done with the current statement. This is a given."
, FlowParagraph "HOWEVER - in a functional context (i.e. in haskell) - this 'one statement after the next computation strategy' is NOT a given, and infact the decision of HOW to compute, and the ORDER that computation occurs is the responsibility of the current MONAD in play."
, FlowParagraph "So without going into how to actually define your own MONAD, lets rather just look at some existing ones and how they are used. For the example above, the perfect solution is called the MAYBE MONAD (surprise!). And it simply says keep evaluating functions bound into the monad until a function evals to Nothing then return Nothing (stop evalutating subsequent bound functions), otherwise return the evalation of the final bound function."
, FlowSourceCode { lang: Haskell, body: "\nmaternalgrandmother2 :: Person -> Maybe Person\nmaternalgrandmother2 p =\n mother p >>=\n (\\m -> mother m >>=\n (\\g -> return (g)))\n" }
, FlowParagraph "This is much cleaner! The bind operator (>>=) takes care of all the ugly case handling for us. If either `mother p` or `mother m` returns Nothing, the entire expression returns Nothing. Otherwise, we get our granny wrapped in Just."
, FlowParagraph "But we can make this even cleaner using do notation, which is syntactic sugar for chaining monadic operations:"
, FlowSourceCode { lang: Haskell, body: "\nmaternalgrandmother3 :: Person -> Maybe Person\nmaternalgrandmother3 p = do\n m <- mother p\n g <- mother m\n return g\n" }
, FlowParagraph "The do notation makes the code read almost like imperative code, but with all the safety of monadic composition. Each line extracts the value from the Maybe, and if any step fails (returns Nothing), the entire computation short-circuits to Nothing."
, FlowParagraph "Finally, we can also express this using Kleisli composition (>=>), which allows us to compose monadic functions directly:"
, FlowSourceCode { lang: Haskell, body: "\nimport Control.Monad ((>=>))\n\nmaternalgrandmother4 :: Person -> Maybe Person\nmaternalgrandmother4 = mother >=> mother\n" }
, FlowParagraph "This is the most elegant solution! The Kleisli composition operator (>=>) lets us compose `mother` with itself, creating a function that goes directly from a person to their maternal grandmother. It's beautifully concise and captures the essence of what we're trying to do: apply the mother function twice in sequence."
, FlowParagraph "All three approaches demonstrate the power of monads: they abstract away the boilerplate of checking for Nothing at each step, allowing us to focus on the essential logic of our computation. Whether you prefer the explicit bind operations, the readable do notation, or the elegant Kleisli composition depends on your style and the complexity of your monadic chain."
]
}