Monads, what are they good for?
Where we put it all together to discover a new lib.
me> So… what’s all this good for anyways? 😩
wat> What is “all this” you speak of? 🤨
me> Functors, Monads, Applicatives… what’s the point?
wat> Ooooh, someone is moody today. 😅
me> I mean, I know Functors are things you can map
on, Monads things you can andThen
on and Applicatives things you can andMap
on… and it feels like whoop-dee-doo, what do I do with all this now?
wat> Because you were not trying to learn all this just for the pure joy of learning useless facts? 😏
me> Kind of. Yes. In fact, I’d rather do this for a purpose, to be honest.
wat> Well, let’s see whether this knowledge helps you discovering a new library… say… the Parser
module.
me> I don’t know… that sounds complicated. 😐
wat> All the better. Let’s open our books at… https://package.elm-lang.org/packages/elm/parser/1.1.0/ :
me> Unsurprisingly, it’s about creating Parser a
instances… that unsurprisingly promise to parse an arbitrary string and either produce an a
value or fail… sounds familiar…
wat> I’d says it looks an awful lot like a more generic version of the Decoder a
type, doesn’t it?
me> Yes… looking into https://package.elm-lang.org/packages/elm/parser/1.1.0/Parser we learn that Parser a
is an alias for a Parser Never Problem a
… which is confusing. How can Parser a
be defined using itself like that? And there’s not even the same number of parameters for each one?
wat> That’s because they’re not the same type. The Parser a
type comes from the Parser
module while the Parser context problem a
type lives in the Parser.Advanced
module. So Parser.Parser a
is an alias for Parser.Advanced.Parser Never Parser.Problem a
.
me> Ok… so, Never
being a special elm type that has no value, I guess a Parser a
will not provide context
, whatever that’s for ; also, I guess the problem
type parameter is for a type that represents problems encountered so the Problem
type must be the way the Parser
module characterizes those problems… so the only type variable that we have to care about is our a
type that is promised?
wat> Yes. We also find a lot of pre-built standard Parser
like int
, float
, …
me> Some of those look funny 🤨 Like:
symbol : String -> Parser ()
wat> What’s so funny about it?
me> Well, what is a Parser ()
for? What’s the point of parsing something if all we get is an empty tuple?
wat> Hmm… remember when we said that Functor, Monads and Applicatives could be seen as a way to wrap other types of values in something like a context?
me> Yes? What’s the context for Parser a
then?
wat> It’s a context that promises to either succeed parsing, or fail… but while parsing, this context is also a way to remember what character your parser is currently looking at.
me> Looks like you’ve just spoiled me the ending there: Parser a
is also a Functor, a Monad and an Applicative, right?
wat> Let’s not go too far ahead, but yes it is. Now, to answer your question, a Parser ()
will not extract an interesting value, but it can say either “hey, this here character is totally what I was expecting there” or “hey, there’s a problem here, this character is not at all what I expected”, and then pass to the next character (or characters) for the next parser in the chain to do its job (or not, because there’s no point continuing when the previous parser in the chain failed).
me> A little bit like when we’re using andMap
to accumulate multiple Maybe
values, when one is Nothing
the end result will be Nothing
no matter what the other Maybe
hold?
wat> Yup. Just like that.
me> Ok. But… if this is an applicative too, where’s the andMap
?
wat> Just look at this:
(|=) : Parser (a -> b) -> Parser a -> Parser b
me> Hmm… it takes an a -> b
wrapped in a Parser
context and a Parser a
to make a Parser b
… that looks like a proper andMap
indeed… but why is it not called andMap
?
wat> I guess because designers of the Parser
module wanted to adopt a pipeline kind of style:
include Parser exposing (..)clownCarParser : Parser ClownCar
clownCarParser =
succeed ClownCar
|= clownParser
|= clownParser
|= clownParser
|= clownParser-- in my days, clown cars could hold more than just 4 clowns
me> I see… so, I guess you wrap your empty ClownCar
constructor in a successful parsing with Parser.succeed
, then you take it for a ride accumulating potential clowns with your |= clownParser
calls until it’s full. Just like we did filling functions with Decoder.andMap
.
wat> Quite. Now, if you have a Parser a
and need a Parser b
?…
me> I can either use an a -> b
function with Parser.map
to pull out the a
from a successful parsing and turn it into a b
that Parser.map
will re-wrap in a successful parser… or maybe go the andThen
route when the a
value may or may not be enough to guarantee I can have my b
. Right?
wat> Right. And that’s how knowing about the Functor, Monad and Applicative stuff is useful. They can help you get to speed with new libraries (provided the authors are adhering to the rules¹).
me> Well, I guess I’m going to have a closer look at all these interesting Parser
functions…
[1] there are rules a proper Functor, Applicative or Monad must observe that we haven’t mentioned yet ; we’ll probably get into them sooner or later, probably when we talk about how to make your own types a Functor, Applicative or Monad.