Category Theory in Elm with Joël Quenneville

Joël Quenneville joins us to help us distill down Category Theory patterns and explore what value it brings us as Elm developers.
March 14, 2022

More of Joël's distillation of category theory ideas:


Hello, Jeroen.
Hello, Dillon.
And once again, we're joined by friend of the show,
Joel Kenville.
Thanks so much for coming back, Joel.
Thanks for inviting me onto the show.
So today we are talking about a bit of a hot topic.
Should Elm devs care about category theory?
Forgive us if we use that term a bit imprecisely,
but I think that that's what Elm developers
often think about when they think about
the area that we're going to be touching on.
So let's dive into it.
So what do we really want to talk about here?
We're kind of talking about some patterns
that we use as Elm developers, or could use.
How would you sort of describe overall
this sort of concept of category theory
or how it might be useful for an Elm developer?
I think there's value in recognizing patterns
in our code and when they repeat.
And then use things we know in one context
and apply them in another.
So basically what we would hope to come away with here
is a better understanding of some tools for abstraction
that we can use as Elm developers.
And maybe just a clearer understanding
of some abstractions we're already using.
We're probably already familiar with these
or don't recognize them as a recurring group of patterns.
So is that how you think about them as a group of patterns
that you can use to identify common abstractions?
Yes, I think I like the term pattern here.
In fact, we used it in a previous episode
where we talked about one of these
what we called universal patterns.
Yes, and listeners should definitely check out that episode
of Refresher. We covered a lot of stuff there,
a lot of it talking about what you might call
the applicative pattern.
It's episode 32, if I remember correctly.
You should actually check that.
Yes, episode 32.
So I've been trying to understand
why is it so hard to wrap our brains around this topic?
I think, I don't know, I imagine a lot of Elm developers
can relate to this. To a certain extent,
we shy away from using some of this academic language,
using these terms in the Elm community.
And to a certain extent, that makes it feel more accessible
for a lot of people, that they don't need to understand
all these terms before they feel like they can jump in
and create a web application.
But why is it so hard to wrap our brains around it?
And I was thinking about, it's almost like these patterns,
they don't have semantic ideas to them,
they don't have domain ideas, and that's sort of the point,
they don't have any ideas.
It's almost like if you were to explain the term
organization to somebody who didn't know what an organization was.
And if they were saying, well, what does it do?
Well, it could do anything.
What's its purpose? Its purpose could be anything.
But there's some sort of set of coherent ideas
you could talk about of how do people organize work.
Are they self organizing teams?
Are they individual?
You can talk about these patterns of how people organize,
even if you don't know what they're organizing to do.
And in a sense, these sort of functional patterns,
it's a similar thing.
They have sort of metasemantics almost.
Do you think that's a fair description?
I would agree.
These things often, I think, are so abstract,
it's hard to describe them outside of their very formal definitions.
When we get a little bit more accessible,
we end up having to hand wave a little bit.
And then we have an approximation,
which works in many, but not all cases.
It's something that if you want to explain,
you usually have to go with a concrete example
right from the start to get it across.
And then you try to generalize, oh, you've got this pattern.
This is how it works for lists.
And then, oh, you can also apply it to maybe,
or you can also apply it to results.
I think as a teaching method, that is absolutely the way to go.
It's going to take a while before you can start seeing
what the greater pattern is and what is part and not part of the pattern.
So for example, you're looking at and then on maybes
and then on results, and you might think,
oh, and then is a pattern for following the happy path.
But then you look at and then on a random generator
and there is not really the concept of a happy path.
And so now your approximation has kind of broken.
So now you have to find something else
that can include the way random does it.
And then you have to find a way that includes the way list does it.
And every time you learn a different one,
you start broadening a little bit that understanding.
And I think it's okay to have approximations
that are good in certain cases and not in others,
because that's how we build our understanding,
how we build mental models.
For lists and then would be concat map, right?
Yes, that's correct.
Yeah, I feel like result and then,
result dot and then and list dot concat map
have two things very differently.
The type signature looks the same,
but otherwise it's pretty much the only thing that is the same,
the same like the laws around it and the type signature.
But what it does and why it does it are very different.
So I feel like it's hard to explain like,
why would you reach for this?
And it's more like, well, this is useful function
and it has this type signature.
And then, well, result has the same one.
It just you don't you just don't use it for the same thing.
But it behaves somewhat alike.
And behaviors that are true across all of these.
And that's where some of the your intuitions can come across really nicely.
And you can build other things on top of them,
regardless of whether it's on maybe or list,
because certain things hold true.
So a classic one that you'll see is, for example, flattening a nested maybe.
You would do and then identity.
That is the thing that is true for any and then function.
So if you want to do a list and you want to flatten it,
you can do concat map identity.
You want to flatten the results and then identity.
So that holds true for all of them.
Right. That's like a law for that pattern.
If we were to get into the jargon.
Yes, yes.
And I think one thing that's interesting to note with these things
is that they're like really formal definitions.
Typically are a combination of some base description of
I said maybe it includes a type.
I forget if you have to have a type.
But typically it's one or two functions.
And then what they call laws.
So things that must hold true for that function,
regardless of what you pass into it.
Yeah. So things like associativity and identity
and sort of like laws of logic and mathematics.
And these things have their foundations in these more.
They're not just for writing computer programs.
They're for logic and reasoning and mathematics.
Understanding where the relationship with mathematics comes from.
Because like you've got mathematics which works with algebra
and set theories, I guess.
And then you've got maybes and then.
And like there's some connection somewhere that I'm missing
and don't know where it is or what it is.
So the whole world of mathematics
is translating pure numbers.
So even if you talk about sets,
a function is a transformation from one set to another.
Right. So from numbers to strings and strings to numbers.
And for instance.
And now once you start thinking of transformations between sets,
that's where some of these more category style things come in.
I want to say, and this is getting to the edges of my knowledge
that it's sort of a layer of abstraction
on top of the idea of sets and functions as transformation between sets.
So there are other mathematical,
I'm going to call them objects,
that can be transformed into each other
with a similar style of relationship that functions and sets have.
And category theory is sort of like a meta framework
for being like, oh, I don't know, groups are to something.
I'm just using random terms here.
These things relate to each other in a similar way
as functions and sets relate to each other.
It's my very fuzzy understanding
of what mathematical category theory is.
My understanding is also that in the programming world,
we've kind of diverged a little bit from that
and that what we tend to call category theory
is mathematical category theory.
Do you have examples where it doesn't map cleanly?
No, because I don't actually understand
mathematical category theory.
Okay, that's fair.
One of the things I wonder about is
how much would these things naturally emerge
if there was no such thing as this sort of
history of category theory and all these terms and concepts
that have been around over the years?
And all you had was the Elm language,
and you had those basic building blocks
for creating programs.
Would the same patterns emerge naturally,
or are they somehow tied to the fact
that we have all these ideas that we developed before
and we try to apply them to this new context?
So someone said, let there be Elm,
and then you invent the future?
My guess is that they would still emerge,
particularly something like a map.
It's such a common style of operation
that someone would write that abstraction
because they're tired of writing a case expression
to unwrap, do a thing, rewrap.
Yep, exactly.
That's my suspicion as well.
And if you think about things like,
your way of doing control flow is,
you have primitive expressions and case expressions
and function calls, basically.
You don't have any exception handling
in that sort of thing.
Well, okay, so then you need some sort of way to do,
and then it's going to pretty naturally emerge
from those basic ingredients, it seems.
And I think that that gets at the conversation today,
is how can we sort of understand
the pattern rather than just,
all right, I do end then, and I do that over and over.
How can we understand that as the relationship
between how you do that with a list
and how you do that with maybe
and how you do that with a result?
I think what Jeroen shared earlier
about working with each individual type,
and then as you work with them,
you start understanding or seeing the greater pattern.
That's the way to learn it.
And then after that, where it gets interesting is,
once you've done three or four,
you can start thinking when you move to a different domain
that has these same functions.
I already know how and then works on maybes and lists
and JSON decoders.
I wonder if it works the same on tasks.
And then at that point,
I think that's what understanding really was.
And I've had that sort of magical experience
of helping out someone in the Elm Slack.
I think this was with the test fuzzers
for writing fuzz tests.
And I'd never used the library
if someone was asking for help.
And I was able to solve their problem
even though I'd never used the library,
did not know how fuzzers worked
because I knew these patterns.
And I've been using JSON decoders.
Yes, it may have been JSON decoders.
It may have been random generators.
I forget which one I compared it to in my mind.
It's a pretty neat mapping
from a test fuzzer in Elm to a random generator
because I'm pretty sure
it's the same thing under the hood.
I think you can just pass in a random generator, right?
But the same overall pattern applies, of course.
I read a couple of explanations of
there was one sort of explain it like I'm five type posts
that I found kind of interesting.
But one of the things it was talking about
was it was saying it's a messy world out there.
And what if we wanted to create
a sort of nice, happy place
where we could not think about that messiness,
where we could just think about simpler things
and then shield ourselves
and so it was saying like,
if you have a result or like a JSON decoder,
there are all these messy things that are happening
with the outside world in a JSON decoder.
All these ways that it could fail
and whether it's failed
and the state of whether it's gone through
and tried to do something that wasn't available,
that's very messy and it's stressful
and we should just forget about that
and happily just give it a function
for transforming our data and call map,
and pass in a plain old function
that takes a value and pretends
that everything is nice and happy
and shields away that complexity
of the ugly world outside.
So I don't know, that sort of clicked with me.
It felt like a pretty good way to describe
I think this goes into what I was mentioning earlier
about approximations and that that sort of picture
assumes that you're talking about situations
where there are potential failures.
So maybe result, to a certain extent,
even JSON decoder or task.
That's not really what's happening
with a random generator
where mapping is not trying to ignore failures.
It kind of does something different.
Right, I sort of got into the specifics
of failure with JSON decode,
but the sort of explanation was talking more
about messiness as a general concept
and it was saying that like that messiness means
maybe the thing exists, maybe it doesn't
or that messiness could be
you will have this thing in the future
but you don't have it now
or the messiness could be
it depends on something from the outside world
and so there is something in common there
that I don't want to think about some sort of messiness
whether it's like uncertainty or happening in the future
or there's something connecting all of those things
even though semantically there are a bunch
of different things, results and presence or absence,
but somehow it's abstracting some sort of messiness
of the world and letting you deal with it
I think what you're referring to as messiness here
is what in slightly more formal context
people will refer to as context.
So it's often referred to a parameterized type
like maybe or random generator
as context wrapped around a value
and that context might be the fact
that it's present or not.
It might be the fact that it's a future value
that you may or may not have,
but it's very kind of hand wavy
and I think context is such a broad word
that it's not always useful to use,
but it's that thing that you've got your type A
oftentimes your type is adding some kind of extra context
about the A.
And naturally if you're dealing with these things,
I mean in a way, if you're dealing with them
in a more declarative way,
if you're dealing with things in an imperative way
you're dealing with a lot of these things
and you don't necessarily need these kinds of abstractions
in the same way.
The abstractions that emerge look different I think.
But if you're dealing with things in a declarative way
essentially what you've got with a lot of these things
that can be transformed in these ways
is sometimes you can't directly apply a transformation.
You know, perhaps with something like maybe you could
under the hood because it's just a wrapped data type
generator or JSON decode value or something like
LMI with context that's able to read things from the outside world
that sort of represents a future value, tasks, things like that.
You can't just map the thing because it doesn't necessarily
just exist for you to interact with it directly right away.
So what you would have to end up doing
if you were dealing with these things without these abstractions
is you would have to basically unwrap a function
over and over all over the place.
So it really does feel like on a desert island
a lot of these same patterns would emerge
if you were working with these basic tools.
And you wanted to do things in a declarative style.
I think you've done an episode on the concept of combinators, right?
We briefly touched on combinators when we had you on last
but we haven't done a full episode on specifically that topic.
Okay, well you've written an article about combinators.
Since many of these category theory ish things
could be described as universal combinators
I'm hand waving a little bit because this is not quite true.
But and then and map and other functions like this
I guess they're not always combining two values
but they're sort of in that world
and they're sort of common utilities
on many different data types.
Also going back to something you mentioned earlier
about different laws and how they're often like
very kind of mathematical logical sounding things
a lot of them have sort of very practical implications
even though when you read the law
it sounds like reading a math theorem.
So for example the map function has a law that says
that mapping the identity function should equal
the mapping identity function.
Basically mapping identity does not change anything
and a really interesting implication of that
is that your map function can change items inside your type
but it cannot modify the general structure of your
what you've been calling the context.
So for example a list you cannot add or remove items
from the list using map.
Otherwise your mapping identity shouldn't change anything
but if you drop items from the list
that thing would be violated.
You can also not reverse the list
if you want it to be a map because otherwise it's not an identity function.
Yes that's right.
So a map makes sure that the list
or whatever collection stays in the same order
regardless of whether you called it with identity or not.
Right and similarly you can't change a just to a nothing
as part of a map or okay into an error or anything like that.
You can only modify the wrapped value
which is a really interesting thing that comes out of that
and if you had implemented as a test suite or a fuzz test
you would find that if your implementation does modify
the overall structure your fuzz test will start failing.
Yeah when you do implement these kinds of functions
of the type that you made do you write fuzz tests
or do you write anything to make sure that those laws are applied and true?
I like to write fuzz tests that follow the laws.
That's a good way to find out that you've implemented
the function according to spec.
Because there's nothing at least in Elm saying that
oh this type has a map function
there's nothing saying that this respects the laws of the map function.
I wonder is it useful to have this to call it like map
if you don't know whether the law is applied or not?
For instance someone could have a map on a list like structure
but that reverses the list because of some bug.
Would you then still call it a map?
Would you just call it something else?
Do you just report a bug?
I think I would probably report a bug on that
the library maintainer.
If it is intentional then I might recommend that they name it something else
because this sort of violates what people expect of the map function.
Yeah even though there's nothing saying
hey this is a map function from the functor name.
Sorry for dropping the f word.
I don't know if we've been avoiding saying these words for now
but there you go.
Now we can say them if we feel like it
or avoid them if we want to.
To clarify functor is the fancy
category theory term for
a type where we've implemented a map function
that fulfills I think two or three laws
one of which is the one we mentioned about identity.
Yeah I think the other one is commutativity.
It's a really fun one that I actually take advantage of pretty frequently.
The idea that instead of
if I have two functions I want to map
let's say I have a list of
I don't know a list of integers and I want to double them
and then I want to add one
instead of mapping once through the list to double them
and then mapping a second time to add one
I can map once and double and add one at the same time.
That's actually a refactor that I do pretty frequently in my own code
and that some fancy compilers can actually do automatically for you
as a performance optimization.
I tried working on that with LMAOptimize level 2.
I actually don't remember where I got it
I probably have a branch somewhere.
It's clearly something that could be done
but the issue is then
whether this map function is trying to respect those rules or not
which we know for the core functions but not for the rest.
For the most part the community has a good intuition of how they should work.
Yeah I agree.
And if they don't you probably get some
an issue or a PR on your repo.
Yeah I would imagine that the fact that
Elm is a pure functional language would eliminate
the idea that things would not follow these laws too.
I feel like it would be pretty easy to not follow a law
if you could accidentally throw an exception or depend on some outside state.
I feel like you can easily, there's a whole reason we have these laws in the functional world.
They're all functional things like
these things must be commutative or the identity function must apply in this way.
So they're not about not throwing errors or mutating state
it's just when you build your pure function here
it's not a specific criteria.
Right but wouldn't, I mean if you throw an error then it's definitely not...
That kind of breaks I think other parts of functional programming
not for the laws.
Yeah it's kind of like when you play Monopoly and you tell your child
hey if you roll a six twice then you need to go to prison
and then the kid just throws the table.
That's a good analogy.
So one really interesting thing that comes out of this very broad definition of
here's some laws and a few functions that must follow those laws
is that for any given type there may be more than one valid implementation
that follows the laws and the signatures and all this stuff.
So there is no, not necessarily a single
for example applicative implementation for lists.
So applicative being like map 2 right?
Yes, map 2.
So in Elm when we map 2 with two lists we're effectively zipping them together
but it's also possible to do a map 2 in a way that's doing a Cartesian product
so all the possible combinations from the two lists
and that's what Haskell's map 2 does
and both of these are completely valid.
In fact Martin Janicek
built a package that has a
and list.cartesian to showcase the fact that both of these are
different ways of having map 2 that work in different contexts.
Yeah and the proof for that is that they respect those laws and that they have
that special type signature right?
Those are the only criteria.
Do they need to match maybe?
In Elm no, the name doesn't have to match.
Yeah no, because there's no concept of functors or anything.
In Martin's package what he's done is just created two modules
with it expose functions that operate on the same data type
that way he can reuse the name in a different namespace.
So you've got list.cartesian.map2
Gotcha, I thought he had done like a map cartesian or map zip.
Yeah, no in this case he just used the namespace and used the same name
and they both act on the same core list type.
So it's not like each of them has that, there's not the concept of a cartesian list and a zip list.
This also happens a lot for one of the simpler patterns called Monoid
which is built around this idea of like combining items.
So I think what you need is you need some sort of combining function
that can combine two values of a type and then you need a sort of empty item
that when you combine it with something else is equivalent to identity.
And for example looking at numbers you can add two numbers together
that's a form of combining and if you add zero you don't change your number.
And so addition for numbers is a monoid
but it is not the only one because you could do the same thing with multiplication
except in this case your sort of base element that when multiplied doesn't change is not zero
it is one because any number multiplied by one is itself.
So and I actually like monoid as a sort of example
in that we often tend to think of these sort of categories as inherent to the type
but it's not really about the type itself it's like a thing you can layer on
and so what you need is like sort of several different component pieces that all come together
and if they're all present then you can talk about the collection as being this category
in this case monoid. So you can see if you have numbers
and zero and multiplication now you have a monoid
but you need all three of those. Numbers by themselves are not inherently monoid
that's not a thing about the type.
Right like you could have some special kind of numbers that you cannot add.
In fact you can define a number type and just not implement the addition function.
Exactly yeah and then it wouldn't be a monoid.
Exactly the interesting thing is that someone can provide a type in a package
and not write some of these functions and so it is not a functor or a monad or any of these other things
but then I pull down this package and in my application I write one of these functions
to act on this third party type and all of a sudden within the context of my code
it is a functor because now we have all the pieces.
That's a great point.
If you have the building blocks to implement that function.
Yes so for example I could create a maybe type
and publish it without the map function on it and then you could decide
well I need to map over it I'm going to write a map function for your maybe type.
And now it's within the context of your code my maybe is a functor
or the combination there is.
I sometimes find it is an easy approximation to talk about these things as adjectives rather than nouns.
So I can say this type is monadic not this type is a monad because it has these functions
so this is like an attribute of it rather than being an inherent quality.
Yeah it's not like you create a type and you say okay this needs to be monadic
this needs to be blah blah blah it's you add the function and therefore it is monadic.
Therefore it is a monad.
I think you can talk about it in a noun sense to say oh I have the maybe monad
but at that point you're not talking about maybe the type you're talking about this sort of
the fact that I have a type and some functions and they all follow the laws.
And I think that gets confusing to people when you talk about the x pattern.
So there's an interesting example that when I realized this it sort of hurt my brain a little bit
which is that the Elm GraphQL selection set is inherently not monadic
because it represents a selection set represents both the request for saying
I would like this data and the decoding to say okay I'm going to turn this into an Elm type from the server
but because of that you can't and then that you can't you can't say get this user's role
and then make a follow up selection set because inherently
well I mean I guess I guess that would represent multiple requests to the server
right but inherently a selection set represents a single request to the server
so it is inherently not monadic not just because it's not exposed
but that operation doesn't really fit in that abstraction
particularly because in the the rules that you set up for what a selection set does
you don't want multiple requests to be done.
Exactly. Everything has to be done within a single request.
Yes. And by the monad laws and then would imply a second request which is a thing you don't want to do.
Exactly. Exactly. So the semantics that it would imply for the outside world
would not fit into the desired semantics for the library's goal
to not the whole point is you don't want to make repeat requests to the server
you want to make a single request to say exactly what you need.
Just to clarify that is not an issue right?
No it's not. It's not like there is no and then and therefore there's a problem with your library didn't?
Right. Well I mean it's just a constraint
and it's just one building block that you can't use and sometimes it is quite convenient
like I don't know when I think about these patterns
in sort of layman terms I think about I'd really like to have this concrete data
not this wrapped data thing I want give me the actual data you've got
and then let me continue on and do something with it or I mean of course as we've discussed
these analogies fall apart because sometimes it's continuing sometimes it's dealing with presence or absence
sometimes it's dealing with failure or success but you know
timey wimey you get the idea something along those lines but you can't do that
you can't say I'd like to take this thing and then and then I'd like to perform this check
and fail if I don't have this thing you just can't do that you have to find other ways to solve it
so like it just changes the way that you approach solving certain problems
I think there's a really interesting thing here that goes back to the why is it useful for
LN programmers to learn these concepts and we can kind of get it just by the signature
but given the fact that someone might know the definition of what and then does
in general or familiar with its signature and that they know that in your library
a selection set represents both a request and parse data with those two facts
I can tell you that and then is going to involve multiple requests but
someone who's that's something that I think came out in the conversation we were saying earlier
but that might not be obvious but someone who's
something that I think came out in the conversation we were saying earlier but that might not be obvious
you'd have to dig through if this was named something else but because it's named and then
and you know how and then works then you can quickly
sort of apply it to the domain that it's in and quickly pick up some information
So I feel like all those functions are mostly there for familiarity
If you see a map function you expect it to behave this way
you expect it to follow those laws that we mentioned and I feel like that's
pretty much the only reason to have those for familiarity
at least in Elm. Well they're just common operations that you end up doing
common transformations that we end up doing on data. But the reason why you would name them
that way or that you would have the arguments in that order
Yes. But yeah obviously like if you want if you have a selection set which contains some data
then map makes sense whether you call it that way or whether you make it
follow those laws or not is a different consideration
but I think it makes sense to have them follow those laws and have that name because it makes it
very familiar to people who know who know etc.
So I feel like it's all about familiarity and just making it easier to
to grok what a piece of code does based on what your knowledge of other pieces of function do
I would say that's the general value of the concept of patterns
Yeah I guess. Even like in the object oriented world right the idea of design patterns or
standardized ways of solving common problems it's much easier for me to look at some code and be like oh that's a decorator
rather than being like huh okay so there's an object that's wrapping another object
and there seems to be some kind of delegation happening here and instead of like really digging into the implementation
I can look at it and recognize the pattern and then immediately I know it's one of those big building blocks
that I've seen in 50 other contexts. So what about when Elm diverged into the
list.concatmap it's not called list.andthen but it has the same signature
and vice versa with things that use andthen
so it seems like in many cases Elm opts for
using more domain language than category theory language
but there are trade offs there. I mean like the
you could imagine a world where we just say let's be consistent with this naming
if it follows these laws let's pick a term for it that we're going to use in the Elm community
and let's stick with that in all of the core libraries at least so let's call it list.andthen
or let's use concatmap elsewhere whatever it is let's stick with it. But it does get difficult to sort of
because again the semantics can feel so different even if the pattern
is the same. Yes I think in fact lists are really tricky
because it's often where you're first introduced to a lot of these things particularly
something like map and if you know your only example that you've seen is list map
you're going to think map is about iteration and list traversal and so when you see
maybe map JSON to code map and it's not about traversing a list
you get really confused. Or at least I did.
Yeah especially coming from JavaScript where the only map function that I know is on arrays
so collections. Yes although fun fact
the promise then method is effectively map on a promise
but it's also an and then. Yes it is also an and then.
So is it what kind of. It's monadic
but the exception part isn't really well behaved
the errors don't really map cleanly I don't know if that breaks any monadic. It kind of does yes
as an approximation you can say that promise then is
like a similar to a monad it's similar to a functor or similar to map similar to and then
because you can do promise dot resolve to get just the unit value
and you can do then on the promise to continue.
Well it gets interesting if you compare it to say elm task which is very similar to JavaScript's promise
where elm task has two very separate functions for map and then if you're looking at some code
and you see that it applies a task map you know that
let's assume these tasks are just HTTP requests you know it's only making one request and then
applying a transformation to the result of that request. Map cannot make a second request
because of the functor laws but. Can you clarify why?
Because if you made a second request
it would end up
let's see I think if you made a second request you ended up with like a doubly nested
task. Yes you would have a task or a task. Right which is actually why
some languages instead of calling it and then we'll call it flat map because it's
do map but with another task and then you've got this nested thing and you have to flatten it down
and so because of the way the signatures and the laws work out when you
see a map you know it's a sort of a local operation it's a safe thing as opposed to if you
seen and then you know okay it's doing some logic potentially and then making another
request whereas if you see a chain of thens in JavaScript you don't know
did we make one request and then just do a bunch of local transformations or did we
do 10 requests and now we get back to another of the functor laws
which is I think what you were mentioning earlier that you were trying to
do with the LLM optimized level 2 where all of those like a chain of maps can be like
flattened down and what you can do is all those functions that were in each of the maps just pull
them out into a separate named function. So in LLM if I see task
and then just task map task map task map like five of these I can just look at all those five functions
create my own named function pull all of that out and say there is one
transformation that gets applied at the end of my request. In JavaScript I
can't know to do that because if some of them are making extra requests that's not just a pure
transformation. So then I'd have to say okay well these two can be pulled out but then I
need another I actually need a then to make that second request and then some more
transformations. So you have to really check each item at a time.
So you could with a promise if it didn't behave as an and then
if it only behaved as a map then you could potentially right. Yes if you saw
if it only behaved as a map and you saw a chain of thens you could reduce it down to a single then and then just
pull out all of those extra things into one function. But it's not safe to do that
if you are potentially making more requests.
And I guess I mean naturally you would reach for the simplest tool you could in a lot of
cases and try to use map where possible kind of like I mean you're in and I
often come back to Richard scaling L maps talk where the one of the
main takeaways is if a function doesn't need more data give it less
data. If a function doesn't need to pass out more data for the
calling code then don't return as much data just constrain things more so
that you can more easily reason about what does this code depend on and what can this code do.
And that same principle I think applies from what you're saying here if you
don't need and then if you can do it with a map that's less cognitive complexity for you to hold in your
head when you're looking at the code or when you're debugging and trying to find where an issue is coming from.
Yes. And in fact I've often seen people use and then when they only need a map
and the classic thing that you'll see them do is and then some pure function
then like adding an explicit wrapper. Right. I want to say is there a
Elm review rule for that? No but it could definitely could.
Yeah. So like a classic case would be something like maybe and then some pure function
and then just wrap the result in just. But that's basically just saying wrap it in just and then
unwrap it because and then is you can think of it as a flat map
and so it's just going to apply map but you wrap it in just it's going to flatten
it back down and you've done extra work for nothing. I might have something
similar to that in Elm review simplify. OK. And if not someone could create an issue I'm sure.
Or a pull request. Yeah. Or pull requests while listening to this episode
because our audience or listeners are very quick.
But I guess to come back to the sort of original question that we're asking right should Elm devs care about category
theory or why should we care about it. One thing that really helps is to
because you can recognize some of these larger building blocks and patterns you can immediately gain some
much deeper insights into how the program works and what it does or doesn't do just by looking at some of these
function names. Right. Yeah. That's a great takeaway.
I feel like in the Elms like I've seen some conversations which I will not be able to remember so I can't give it an example
where someone asked like is it possible to have a
function that does this or does that with this type
and people say well no that is not possible because you're missing
some constraints on your type or on your functions or it is possible and
here is the type signature that you will see in other places. So I feel like some people are able to recognize
when something is possible when something is not possible very quickly thanks to this.
I'm thinking like I don't know if I'm pulling this out of thin air but like
contravariance could be one example maybe. I don't even know where that one is.
But yeah where you have like B to A as a function
and then you can construct something. I'm going to stop here because I'm
swinging. There's definitely cases where you can use it to say well here are standard solutions to a particular problem that follows one of these patterns or sometimes even built on top of these patterns.
So you have a list of maybes and you want to accumulate them into a single
maybe with a list of values. That is the thing that is
really interesting about this. I think it's really interesting to see
that you can actually generate a list of values into a single maybe with a list of values.
That is the thing that is a solved problem for actually
genericize from pretty much any types as long as they implement map2.
So you know as long as your type is applicative then you can do this
kind of sequencing of values together or combining.
You can either sequence or combine for this kind of operation.
So this might be a big ask Joelle but can you try to break down a few of these patterns like you just did with this sort of idea of like OK when you see map2 you know you can combine things.
What are some other patterns and some other things that you know you can do when you have those patterns.
One that I like and again this is an approximation it's not always true but I typically think of
monads as being serial or sequential and applicatives as being parallel.
So if you're doing a map2 or map3 or map4 the important thing is that all of the things you're combining together are all independent of each other.
So in theory if you're doing a map4 with four different HTTP requests those could be executed in parallel.
Elm does not for the moment. That's an implementation detail. They've chosen to do it in sequence.
But in theory they are independent and can be executed in parallel in a way that doing a bunch of and thens for make one request and then make another request and then make another request.
They must be sequential because you can't make the second request until the first request resolves because you need data from the first request in order to even construct the second one.
So there's an inherent dependency chain in and thens that you don't have with map2 or map3.
Now there's a little bit of fuzziness there in that that's only true with regards to the type variable that you're working with.
So for example parsers they're all independent of each other for the thing that they're parsing.
So the output of one parser won't impact the value of the second parser but they're all operating sequentially on the underlying string.
So there is a sort of inherent sequentiality to them but they are independent of each other.
So that serial versus parallel model falls apart in some cases.
But for many or most of the and thens and map2s that I've experienced it's a useful distinction to keep in mind.
Yeah I mean at the very least you know with and then just based on the type signature you know you're going to be able to take the concrete value and you're going to be able to have that and then give something else back.
That's like what the signature says.
So if it is something where it's performing operations you know it's going to need to be sequential because you actually have a resolved value at that point.
But the naming does get interesting there right because the word then does imply time and you know maybe with a list we're not dealing with time and so maybe we call it concat map and it gets a little fuzzy doesn't it?
Right right and even something like maybe right you're not dealing with something that's in the future.
There is an implied sort of fact that the second maybe is going to be resolved after the first one because there is that order that's there.
So maybe then kind of makes sense there.
Right, but the the general thing you're doing is you're taking some how resolved or concrete or present value or whatever that that fuzzy thing represents and you're able to return a new one of those with and then so okay cool so that's sort of this.
When you have a monad you have and then that's what you can do. What are some of these other patterns?
So we talked a little bit about monoid earlier the idea of we have some combining function and we have sort of a base case that always does nothing when combined with another value.
Is there a username for the combining function? Do you know?
What is it? The... oh I forget what it is.
The base case is sometimes called mempty in Haskell and they have a name for the other one.
I don't know if those are standard names or if that's just the Haskell names for it, but what's interesting about this is that it may have actually kind of set off a light bulb in your mind.
The idea of a combining function in a base case more or less what folding does.
And so if you have a monoid, then you can inherently fold collections of that or lists of that type using that monoid.
So most of the classical sort of fold function examples will show like summing a list is effectively just applying that monoid to things.
But you can also multiply things.
Something like asking are all values in a list true according to this predicate or are any of them true?
Now you're effectively folding the Boolean operator or or the Boolean operator and.
On Booleans and is a combining function and anding a value with false returns sorry, ending a value with true returns whatever your value was.
So ending values is a monoid and same thing with or.
Orring a value with true returns self and or is a way to combine two Booleans.
Orring a value with false returns self, orring a value with true returns true.
Yeah, yeah, I got it reversed.
Right, right. Yeah, no, that's 150% chance.
And I got it wrong with both.
You were very unlucky.
So like would a set be another example of that?
You can have an empty set and you can union a set.
Yeah, you right.
Yeah, you can union with that.
You cannot insert an empty element, but you can union two sets, right?
So you could form a monoid for sets with union as your combining function and empty set as your, I guess, identity value.
You could. I'm not sure for some of the other set operations if intersection works or not.
I mean, I think effectively you can you can think of union like or an intersection like and.
Yes. So it's probably possible.
Yeah, I have to think about that.
Where this is really interesting is that functions are themselves monoids because you can combine two functions with function composition.
Right. And you can combine them with identity.
Yes. And I've actually used this on.
In fact, this is not even like a functional programming thing.
This is a Ruby project.
And I had to do this complex search form with a bunch of search parameters.
And the way I'd often solve this in Ruby was just like a bunch of like if else statements like if this parameter is there,
try to filter your results through this way. But then like all the combinations get really weird and it gets really messy.
So instead, what I ended up doing was creating a bunch of query objects, which are you can think of them almost like functions,
but they operate on the database and made them composable.
And then based on the parameters in the URL, I would just construct a bunch of these and then fold through them to combine them all together,
effectively just to compose them with the empty query as my base value.
That's how I built up my search.
At the time, I didn't know about monoid. I didn't really know that much about folding even.
It just kind of worked out. But years later, when I learned about monoid, I was like, oh, that like really cool search thing I did in a Rails project once.
That was monoid plus folding.
It's worth noting that folding doesn't always have to be on a monoid. I think that's a common misconception.
You can fold things that are not monoid because there are some operations that don't follow the monoid laws where your base case isn't necessarily the identity.
And so folding is a broader set of things, but monoid are always foldable.
Yeah. So I'm currently thinking that if you provide a functor, so if you provide a map function and it is a monoid, would you say it is monoid?
Sure. Yeah, we can say it's monoid.
Yeah. So if it is a monoid and it has a map function, so it's functor, then you can implement like map reduce strategies, right?
Oh, that's interesting.
And now I'm realizing that in Elm Review, when I ask authors to create the rule, I ask them to provide something that actually looks like give me an initial value, kind of a neutral value.
For the context?
Yeah, for the context, the project context.
And I also ask them to fold them, a way to fold them. And internally, then I do like map reduce to combine these together.
So multiple project contexts into one project context, which allows me to do pretty nifty things under the hood.
So I think this may be a little loose with the terminology here.
When I'm talking about that monoids can be folded, it's not the monoid itself that can be folded.
It's a collection of them.
So, yes, numbers are not foldable, but a list of numbers is foldable.
Yeah, two numbers are foldable together into one.
Well, two numbers can be combined monoidally with addition.
A list of numbers can be folded using the addition monoid or folded with the multiplication monoid or however you want, whichever one you want to use.
Right. Given that you have an operation that allows you to combine them, you can fold them.
Given that you have a monoid for some values, a list of those values can be folded.
So to your example earlier, it's your collection, your list that's a functor and the items in the list that are monoids.
And then you can do your map reduce.
Yeah, this gets really powerful once you start noticing some of these pieces.
And sometimes you notice like, oh, my type could be made or is an operation that would be really nice if these things were monoids, but they're not.
But also, I know that they could be if I implemented one function.
And so it's like you implement one function, then everything else falls into place and you can write a really nice program.
Right. That is sort of the beauty of patterns, isn't it?
Is you don't have to reinvent the wheel.
It's sort of like this is going to pop up again and again and I'm going to have to think really hard.
How am I going to solve this problem?
And then I'm going to come up with the same solution.
If only I sort of understood that pattern better.
So that is something I would like to strive towards to to be a little more versed in these patterns so that I can sort of pull them out as needed more and more easily and add them to APIs as needed.
And sometimes you already have like all the machinery in place and all you're missing is that one cog.
But if you don't know that it's that one cog you're missing, you're going to rebuild the whole machine.
Instead of being like, oh, I just need to implement this one little function and then everything else is just going to work.
Right. Yeah, I wonder, like, you know, I mean, when I built Elm GraphQL, I was very new to all all these things.
In fact, like I literally read an article, like I found an article explaining what a phantom type was.
I'm like, that's what I need. And that day added that as I was like exploring building this API.
But yeah, it took me like it would have been nice to know that, hey, having the ability to just have an empty thing that when combined with anything else is itself, that would be cool.
And then you have a selection set that empty and you can.
So I suppose that would be a monoid because you can take a selection, an empty selection set and combine it with another thing.
You could fold over that. That's useful knowledge.
So what is your take on names? So there's a lot of discussion around this.
So if something has and then do you call it and then able or do you call it a monad or how would you teach it around names?
Oh, that's a really hard question because I think it is useful to have a name for things that have and then.
I think that having a name like monad is not useful for beginners, people who are learning.
And in fact, even talking about it in terms of some larger pattern is not useful for beginners.
Because it didn't grasp it in their head yet.
And now you're trying to see like what are the things in common, what are the patterns?
And it's just so abstract that you tell them, oh, it's just an and then function that follows these laws.
And it's like, well, that's too simple. What does it actually mean?
And then you get stuck in all these like really high level things that just kind of make you spin in circles as opposed to just being like, oh, here's how I combine.
Maybe here's how I combine results and then over time building up an intuition for what it works.
Eventually, you're going to want a name for all of these things.
And either just to recognize the path, like put a name on a pattern you've seen sort of emerge, but also to having conversations with other people or with yourself to say, oh, this thing here, if I make it and then all of a sudden a bunch of other pieces fall into place.
Or you're doing review for someone and you're saying, you're doing a lot of work here and it looks like you're chaining things in a monadic sort of way.
And I've used this with people who know what the term means in nonfunctional programming contexts because it is a very useful term.
I guess there's also just the fact that if you want to know more about the pattern, if you want to research more, then googling and then is not going to give you much, but googling monads will give you a lot of resources.
Yes. Unfortunately, most of those are not going to be helpful.
That's a different thing.
Yes. Yes. I think also what's sometimes tricky with these things is that a lot of the literature around these terms is written in a very kind of formal language.
Like it's written almost by academics for academics.
I remember I gave a talk last fall on folding and a particular sort of style of folding, which I didn't mention in the talk, but it's called a catamorphism.
And anything that I read on folding and catamorphism has got really deep.
Even the ones that are claiming to be like written for programmers in an accessible way.
And you get a few paragraphs in and we're talking about how F algebras compose and arrows.
I'm just completely lost.
So sometimes when you dig in on the terms, most of the material that's out there is not written for people like us who are trying to write programs in the real world.
And I wish there were more tutorials out there that try to give more of a practical introduction to some of these concepts.
And I feel like you've written a few.
I've tried. The talk I gave about folding was exactly that.
I was trying to say, let's take these concepts and make them accessible to people and not worry too much about the formal definition of what a catamorphism is, but instead, how is it useful?
My example was working with trees and particularly inverting a binary tree on a whiteboard in one line of Elm.
That was my my selling point.
Yeah, we'll definitely link to that talk.
Yeah, it is. It would be so nice to just have it written in plain English in one place.
Like, here are all these kind of common patterns.
Here's what they look like in Elm.
Here's here are the kinds of problems you can solve with them.
This conversation has definitely been illuminating. So thank you so much for coming on to illuminate these things for us and dive in.
It's been a pleasure. Thanks for bringing me on.
Are there any concepts that you would recommend people learn first?
And do you have any resources? Because you said it's very hard to find resources.
So if you have good links, that would be useful.
I'm a big fan of just digging into maybe.
I think it's a great place to learn, particularly because maybe exposes its constructors.
So you can manually unwrap it and manipulate it and sort of see how it works under the hood.
An exercise I've done with people before is actually have them implement their own map and then on a maybe to see how that works.
I also have an article called the mechanics of maybe that sort of looks at it less from a patterns perspective and more from a here.
You're new to maybe here's some common situations that you run into.
You would write a case expression like this. And because it's such a common situation, the library provide the helper for you, which end up being things like map and and then.
But the intuition, even for just the difference between map and and then.
I think you touched on a little bit, Dillon, the idea that not only transforms the value inside does not produce a new maybe whereas and then does and sort of what the implications of that can be.
Right. It is. I really like this idea of using maybe as an exercise to let's build these up from scratch.
One thing that's a little bit elusive is maybe doesn't have a succeed function.
I mean, it does. It's just not called that. And it's not a standard function. It's a constructor function. It's just.
And so you have to really squint your eyes to see those patterns there.
Yeah. And the reason you bring it up is because the definition, the strict definition for what a monad is requires an and then function and some form of constructor.
Right. We've sort of not mentioned that out loud on the episode, but that's what the formal definition says.
And for some types that don't expose the internals of the type, for example, task or JSON to code, they will often expose a succeed function, which is a constructor.
Right. Exactly. Yeah. So, Dillon, I'm curious if you used any of these patterns and concepts when you were designing the own GraphQL library.
You know, I wish I even knew what these things were back then. They were much fuzzier in my mind at the time, like just conceptually in category theory and those things.
But even just the patterns themselves were less clear in my mind. So I had no idea about these things.
And the original Elm GraphQL, which was called GraphQL, it had these core concepts of like, you know, making it safe to query for your GraphQL data and having phantom types to protect that you're querying in the right context in these things.
But it really didn't have any of these properties that we've been talking about of monoids and functor and all these things. So there was a selection set and there was a field.
And those were two different concepts.
Did you have that sort of nice pipeline API back then? So like the selection set with build things out?
There was. There was selection set with.
And so that, I guess, just to clarify for our listeners, is applicative. So selection sets could be combined applicatively.
But you also have this concept of fields which could not be combined applicatively.
Well, now here's the thing. I would venture to guess that it would not be applicative because it wasn't applicatively combining it with its own type.
You would build up a selection set. So you would say, here's a selection set. And I can say with field, with field, with field. And it takes it on to a selection set.
You're right. That would not be applicative.
Right. So the reason it did that was because I was trying to do, I needed to make sure I had a unique way of querying for them.
So in GraphQL, in a selection set, you can say, give me, you know, the avatar for user one, two, three.
But you can say you can pass arguments to your fields in GraphQL selection sets.
So you could say, well, I want the large thumbnail and I want the small thumbnail.
Well, those would both be thumbnail, the thumbnail field.
And that comes back under a JSON key of thumbnail.
But those both come back under the same JSON key.
So I need a way to uniquely be able to get each field that you're requesting.
And because of that, I would say, well, if I'm adding on a field that already exists, so I have thumbnail and you're adding thumbnail again.
So when you say with thumbnail small and then with thumbnail large, now it's going to add thumbnail too.
It's going to add an alias the second time you add something. So it's no longer unique.
And so because of that implementation detail, when I was trying to figure out how to really in hindsight,
I was trying to figure out how to obey these laws and building up a selection set where I could combine things, where I could do map two.
And I knew that I wanted map two, I guess, because I knew that I could do nice things with JSON decoders.
And so that was an obvious parallel. But I didn't.
Yeah. So in hindsight, that's really what I was trying to do, was trying to figure out how to obey those laws.
Interesting. It sounds like you came at it more from a developer experience perspective.
But somehow that brought you exactly in line with some of these classic category theory constructs.
Exactly. Yeah. Yeah. And in hindsight, that is really, really cool that that was just I wrote I wrote a blog post about I think it's called
how guides you towards simplicity and how I essentially had to have a deterministic way to uniquely hash these fields when I added them in.
And that made it so it was order independent. So you can add you know, you can have a selection set where you add a thumbnail and then you can add another thumbnail later.
And it doesn't care because it's already uniquely created a field alias.
So it's never going to collide. That's that's really cool. And it goes back to sort of that one property we discussed earlier that applicatives tend to be order independent.
Right. With a little asterisk. There are some some edge cases like parsers.
I feel like there's a lot of asterisks around those laws. Not the laws, but the functions and how you.
Right. Right. Because order independent is not part of the laws or at least not order independent in the way that we think of it.
I think there are laws around things like commutativity, which effectively mean order independence in a certain strict sense of the term.
But yes, for in general, it's easy to think of applicatives as being order independent. And that's exactly the behavior you wanted.
Right. Yeah, exactly. And things just bloomed at that point. It just felt like the the whole library before that, it became so much nicer to work with.
And now so there used to be this concept of a fragment of a fragment selection set.
So so GraphQL has concepts of a fragment and you could like build these fragment selection sets.
And I don't even remember exactly how you did that. It is the kind of thing that it was like you had to build up a fragment and then explicitly combine it with it and it would duplicate these things.
And it was a very difficult problem because how do you combine two selection sets when you've already sort of built up the decoder that's expecting to be querying for the JSON that comes back from GraphQL?
But now you you've already sort of built up this decoder. Now you add some some more things to the selection set.
But now you need to change the way you're querying for that data. So it's very messy.
So suddenly the whole concept of a fragment went away and you could just say, well, all you've got is a selection set.
There's no such thing as a field. There's no such thing as fragments.
You've got a selection set which can be empty, can be a single field or it can be a collection of fields.
And suddenly it became so much simpler to work with and the fragment just becomes expressed as a selection set.
So I wonder what questions you could have asked yourself to come to the same conclusion earlier.
Hmm. Like, yeah, that's a great question. Yeah. Like what would have made me realize these things?
I mean, looking back on it now, I guess if I had some more knowledge of these category theory ideas in a way that I could understand for how do I help users solve problems?
And that might have pushed me sooner to kind of find ways to follow these laws.
That said, there were also certain technical things that I didn't have those insights of how do I make it deterministic?
That it's always it doesn't matter what order you combine things in.
But maybe that's maybe that's a takeaway is just if you're having trouble combining things, figure out how to make it order independent.
And if I if I just said, OK, I'm sure I can find a way to do this and focused on that, maybe I would have gotten there sooner.
I think it's interesting. You mentioned you wrote a blog post about how Elm guides you towards simplicity.
And you asked a question at the beginning of this episode where you said, if we didn't have these category concepts and we just started with Elm, would we sort of independently rediscover them?
Would they sort of always emerge? And I think with the answer you just gave here, you're saying yes.
That's my intuition. Now, that said, in a way, I had a blueprint. I had this obvious point of comparison.
Right. Because a graph QL selection set is kind of like, you know, it's it's sort of a JSON decoder on steroids.
It's like a super powered JSON decoder. So there's an obvious API to try to match and say, hmm, JSON decoders can map to JSON decoders can succeed.
Wouldn't that be nice if I could do that? So it's hard to say what the API would have been if it wasn't for the existing example of JSON decoders.
But I do imagine that a lot of the same patterns would have emerged.
I mean, I guess in some way, this is what happened at some point in the history of computer science.
Right. Anyway, so. Right.
Us being here talking about this is proof that this is the way it would have gone, potentially.
I think the biggest insight was when I realized that the concept of a field and a selection set could be the same.
And that the thing I needed to be able to implement that was to not care about order.
And and when I just sort of stubbornly focused on that and said, there's got to be a way I'm going to try to find out a way to do that.
And I'm going to explore every option. For a while, I was exploring, do I need to sort of hold some context of what fields have been queried and lazily build up the decoders?
And can I even do that? And but eventually I just kind of held with that goal and and it took me there.
What about Jeroen? What about Elm Review? Are there any places you think you could have sped up the way you developed certain parts of that API if you'd had these patterns in mind?
Probably a few. I know that I have something called a context creator, which is basically an applicative, I think.
Yeah, it's an applicative because you you start with some with the function and you say, I want this, I want that, I want this and then something to construct it to combine all those information.
I remember that was a bit tricky to implement. I'm not even sure that it turned out exactly how I wanted it to be.
But yeah, that would have been helpful at least. And then internally, I'm pretty sure I've implemented plenty of category theory things that are just not exposed, but that would have been helpful for me.
It's quite easy to even sort of accidentally implement these things, right? Because there's such useful types of functions.
There's almost like a gravity pull towards those sort of optimal points. So the odds of you're implementing a map if you didn't know what map was are pretty high.
Yeah, it's also because we don't have that many ways of building things. I mean, we don't have mutation, so that reduces a lot of what we can do.
That's true.
We don't have for loops. So yeah, how do you map things from list? Well, yeah, you're going to reinvent mapping at some point.
Yeah, it would be cool to maybe that would be like a good write up or something is exploring which of these patterns do APIs that we've built inadvertently implement.
I'm sorry, I invented applicatives again.
That's a great commit message right there.
Whoops, I created a monad.
Is this like the phenomenon of like things becoming crab like in the natural world a lot?
I'm not familiar with this word.
I have become familiar with that one recently and I keep hearing about it ever since.
Basically, the idea that all animal species tend to evolve into something that very much looks like a crab.
So basically, a crab is like kind of a perfect being in a way, an optimal being, and everything just gravitates towards that.
Is it corollary that all programs will eventually be migrated over to REST?
Very probable.
It's all making sense now. All right.
They chose their logo well, at least.
I really like that idea of getting to the basics and trying to derive these things by solving a real problem.
Are there any references for these more formal things?
Do you sort of do what we all do and click a bunch of links and have a thousand tabs open in your browser?
I've written a few articles digging into that.
I've written one on two perspectives on map functions.
So sort of two mental models that I hold.
I've written one about Elm's universal pattern.
We use that as an inspiration for a previous episode.
So those are all, I think, more practical entryways into these concepts.
I also made one on what happens when you run out of map functions, which is digging into what applicative is in a very kind of concrete use case.
So those are all, I think, useful articles for getting some understanding of what these things do in a very practical and in this case Elm centered environment.
Yeah. Yeah. Well, link to all those. Those are great resources.
Awesome. And where can people follow you and learn more?
And any other links you want to share?
Yes, they can follow me on Twitter.
I'm at joelken, J O U L Q U E N. And I publish a lot of articles on the Thoughtbot blog.
So that's Thoughtbot, T H O U G H B O T. I think I spelled that right.
Slash blog. And then if you want to find mine specifically, slash authors slash joel dash kenville.
It might be better to have an actual link then.
Yes, we will add that.
Look at the show notes for the link. Great. Well, thanks again, Joelle, for coming on and pleasure as always.
Thank you. Great to talk with you all again.
And Jeroen, until next time. Until next time.