With all the buzz of late, Functional Programming this and Composition that, terms like Functor
and Monad
may have crossed your feeds and left you wondering, "what the heck are these people even talking about?". With such strange names and possibly even more foreign explanations that require a deep understanding of Algebra (the abstract kind) and Category Theory, it may be hard to see how these types of constructs fit in our comfy, day-to-day JavaScript code.
In this series we'll explore, from a JavaScript programmers point of view, what a Monad
is and how they can be used to great effect in our everyday code. We will be focusing mostly on the usage of these types and not the theory behind them.
So for instance, instead of working to understand the following definition:
A
Monad
is aMonoid
in theCategory
ofEndofunctors
We will work toward understanding a more practical definition:
A
Monad
is a data type that allows for sequential application of its "effects" or "embellishments" while mapping its underlying data.
Now, while the second definition still may not be clear right now, I think we can agree that working to understanding those words and the meaning derived from how they all fit together seems a bit more approachable.
Understanding the first definition is critical when we venture out and create our own types. Although if you are anything like me, I like to get my hands dirty and build an understanding by first having a play with things and applying the theory when I have a good intuition of how use them. There are a slew of types already implemented in the wild that we can blissfully play with...without understanding the Maths behind them.
These posts assume an understanding of not only the JavaScript language, but how "currying", "partial application" and "function composition" is accomplished in Javascript. If you feel a bit fuzzy on these topics, there are many resources available on the webs to get you sorted out.
So without further ado, lets get cracking.
Part 1: The Algebraic Data Type (ADT)
Many times when people say "I used this Monad
for this, or that Monad
for that", what they really mean is: "I used this Algebraic Data Type (ADT) for this and that ADT for that". When looking at the code they are presenting, you find that they never touch the Monad
part of the type, or in some cases the type is not even a Monad
.
I would like to start things off by clearing up this point of contention with me. It seems like a minor thing, but I have found calling things a Monad
when we really mean some ADT tends to lead to confusion when we are starting to build our initial intuitions around Monad
s and other aspects of a data type.
Before we can begin to understand what makes an Algebraic Data Type a Monad
or not, we need to first get a feel around what an ADT is. The best way I can think of to broach the topic is provide a simple definition as to what an ADT is. Then demonstrate how an ADT in JavaScript is used in contrast to a (hopefully) more familiar imperative implementation.
Let's take a look at the data we will be processing with our examples:
// data :: [ * ]
const data = [
{ id: '9CYolEKK', learner: 'Molly' },
null,
{ id: 'gbdCC8Ui', learner: 'Thomas' },
undefined,
{ id: '1AceDkK_', learner: 'Lisa' },
{ id: 3, learner: 'Chad' },
{ gid: 11232, learner: 'Mitch' },
]
The data is a mixed Array
that could contain values of any type. In this specific instance we have three types in play: POJO
s (Plain ol' JavaScript Object) of varying shape, a Null
instance and an Undefined
instance.
Our examples will be defined with the following list of requirements:
- Accept any value of any type at its input.
- Unless the data is an
Array
with at least one valid record, an emptyObject
will be returned. - Return an
Object
of valid records keyed with a validid
from the included record, effectively filtering out any invalid records. - We define a valid record as an
Object
with aString
keyed withid
. - This function does not throw, no matter the input, and provides a reasonable default in the empty
Object
it returns.
From these requirements, we can implement an imperative function that does the following:
- Verify input is an
Array
, return an emptyObject
if it isn't. - Declare a
result
accumulator for building our final result, defaulting it to an emptyObject
. - Iterate over the provided
Array
and do the following for each item:- Validate the item against our record criteria
- If passed, add the record to the result, keyed by the
id
value on the record. Otherwise do nothing.
- Return the
result
.
With a few helpers to help us with some type checking, we can provide an implementation like this:
// isArray :: a -> Boolean
const isArray =
Array.isArray
// isString :: a -> Boolean
const isString = x =>
typeof x === 'string'
// isObject :: a -> Boolean
const isObject = x =>
!!x && Object.prototype.toString.call(x) === '[object Object]'
// indexById :: [ * ] -> Object
function indexById(records) {
if (!isArray(records)) {
return {}
}
let result = {}
for (let i = 0; i < records.length; i++) {
const rec = records[i]
if (isObject(rec) && isString(rec.id)) {
result[rec.id] = rec
}
}
return result
}
indexById(null)
//=> {}
indexById([])
//=> {}
indexById([ 1, 2, 3 ])
//=> {}
indexById(data)
//=> {
// 9CYolEKK: { id: '9CYolEKK', learner: 'Molly' },
// gbdCC8Ui: { id: 'gbdCC8Ui', learner: 'Thomas' },
// 1AceDkK_: { id: '1AceDkK_', learner: 'Lisa' }
// }
As we see, we have a strong implementation that meets our requirements and responds to any input we give it as expected.
As for our ADT implementation, we will be leaning heavily on the crocks
library. Even though JavaScript is a fully functional programming language, it lacks some structures that appear in other languages that are not general purpose languages, but are strictly functional. As a result, libraries like crocks
are typically used for working with ADTs.
Here is an implementation that implements the requirements using ADTs:
const {
Assign, Maybe, composeK, converge, isArray,
isObject, isString, liftA2, mreduceMap, objOf,
prop, safe
} = require('crocks')
// wrapRecord :: Object -> Maybe Object
const wrapRecord = converge(
liftA2(objOf),
composeK(safe(isString), prop('id')),
Maybe.of
)
// mapRecord :: a -> Object
const mapRecord = record =>
safe(isObject, record)
.chain(wrapRecord)
.option({})
// indexById :: [ * ] -> Object
const indexById = records =>
safe(isArray, records)
.map(mreduceMap(Assign, mapRecord))
.option({})
indexById(null)
//=> {}
indexById([])
//=> {}
indexById([ 1, 2, 3 ])
//=> {}
indexById(data)
//=> {
// 9CYolEKK: { id: '9CYolEKK', learner: 'Molly' },
// gbdCC8Ui: { id: 'gbdCC8Ui', learner: 'Thomas' },
// 1AceDkK_: { id: '1AceDkK_', learner: 'Lisa' }
// }
One of the differences between the two implementation that I hope was noticed is the lack of familiar flow control and logic patterns in the ADT implementation. Things like for
loops and if
statements do not appear once in the second implementation. They are still there, of course they are still there, but when working with ADTs we encode these flows/logic in specific types.
For instance, notice that safe
function that is used in a couple places? Take a look at the predicate functions passed to the first argument of those calls. Notice that the same checks are being done there, but instead of an if
we are using the safe
function that returns an ADT called Maybe
.
Another thing you may have noticed is the lack of state anywhere in the second implementation. Every variable declared was a function, not a single JavaScript value in sight. We used two bits of state in the original implementation, result
to put together our final result and a little helper called rec
which just cleans up the code and keeps us from having to reference the indexed value from the Array
.
We were able to get rid of the need of the for
loop and the result
variable, by using the function mreduceMap
to fold each record over an Assign
type. Assign
lets us combine Object
s similar to the way Object.assign
does in vanilla JavaScript, removing the need to keep track of an accumulator like the result
Object
. So now that we have a means to accumulate, we can then remove the for
loop by leaning on mreduceMap
.
The Maybe
, Assign
, fold, etc. stuff does not need to be understood right now. I only mention them because I want to communicate that every pattern in the original implementation is present in the ADT version, there is no magic going on here. When we code with ADTs, we remove a lot of the mechanical bits like accumulation, logic, control flow and state juggling by encoding them in ADTs and let the types take care of all the "plumbing" for us.
The last thing I hoped was picked up on is how we are using what looks like a fluent api for chaining our operations together in the functions mapRecord
and indexById
. Seeing code like this may make us believe that we are working with traditional Object
s and classes like a typical Object Oriented Programmer might. It is even reinforced when you hear these operations called methods (all the crocks documentation does this). These intuitions and misleading characterizations can get in the way of how we understand the way that ADTs are used in our day to day code.
Next time we will dig a little deeper on ADT usage by exploring how ADTs are not Object
s in the sense that an Object Oriented Programmer would view an Object
.
Exercises For Fun
- Take the first POJ (Plain ol' JavaScript) function and remove the
for
loop by using thereduce
method available onArray.prototype
. Take note on what happens to theresult
variable and how the default value of{}
is applied. - Take the first POJ function and, without using timers (
setTimeout
orsetInterval
), refactor it to be the MOST INEFFICIENT implementation you can think of. As you refactor, think about what you picked it as the MOST INEFFICIENT. - Using either the first
POJ
function or your refactor from Exercise 1, identify the discrete actions/transformations that could live in their own functions. Then create those functions and refactor the main function to use them.
Additional Exercises (Also For Fun)
- We used a third-party library's type checking predicate functions for doing our type checks. Pick one of the predicates we used and implement your own version of it, throwing different values of different types at your implementation and see if it behaves as expected.
- If you happen to be versed in libraries like ramda or lodash-fp, implement the same behavior in a function using just the library you are familiar with. Compare the result of your function with the following pointfree version of the above ADT version:
// wrapRecord :: Object -> Maybe Object
const wrapRecord = converge(
liftA2(objOf),
composeK(safe(isString), prop('id')),
Maybe.of
)
// mapRecord :: a -> Object
const mapRecord = compose(
option({}),
chain(wrapRecord),
safe(isObject)
)
// indexById :: [ * ] -> Object
const indexById = records => compose(
option({ error: true }),
map(mreduceMap(Assign, mapRecord)),
safe(isArray),
)
Top comments (13)
Well, first of all, welcome. It's nice to see more functional folks here.
I see it like this.
You write, you gather feedback, learn from your mistakes and then you write a better article.
As a novice in FP I would like to see things like: refactoring someone's imperative code.
I started writing my comment without reading the article. So I thought instead of putting it off for later I'll read it now.
At this point I've read up to this:
These posts assume an understanding of "currying", "partial application" and "function composition". If you feel a bit fuzzy on these topics, there are many resources available on the webs to get you sorted out.
I understand those concepts exactly (maybe not in JS, but certainly in Elm and Haskell. I guess I can transfer my knowledge to JS).
Let's continue reading.
But I also have to warn you that I've already read about monad on the futurelearn course about Haskell. But I can't tell that I understand the monad or that I can use it.
// data :: [ * ]
const data = [
{ id: '9CYolEKK', learner: 'Molly' },
null,
{ id: 'gbdCC8Ui', learner: 'Thomas' },
undefined,
{ id: '1AceDkK_', learner: 'Lisa' },
{ id: 3, learner: 'Chad' },
{ gid: 11232, learner: 'Mitch' },
]
Why do you have null and undefined here?
Here's where I'm getting lost.
Our examples will be defined with the following list of requirements:
My questions are:
Why such requirements.
Continuing.
At this point I briefly looked at Crocks.
And now I started retyping:
is gid purposeful or a typo?
And why you have some ids like numbers, a single number, or a mix of letters and numbers? Because data like this doesn't make sense to me. Is this how JS world looks like?
At this point I went to bed to lie down. I contemplated and when I returned I deced to look you up on twitter. Didn't find one.
Looked at your Github though.
You are the creator of Crocks!
Interesting.
At this point I'm watching your youtube video "pointfree and combinators".
Comb (ai) nators - is really a weard way to pronounce that word.
At this point I jumped around the article.
I have this to say:
I didn't think that functional JS would be that weird.
But I also understood that the purpose was to clean the data. I would actually start the article with that. Like we have some unclean data and we are going to clean that up.
You have actually demonstrated that but in the middle of the article. The question is? Would it be better to start the article with that? In my opinion it would.
I'm used to challenges on Codewars. It's such a good thing that they provide input and outputs. It's the first place where I look.
P.S. I rethought my earlier statement that functional JS is weird. It's actually not that weird.
Wow that is some amazing feedback. Thank you for taking the time for putting it all down. I cannot tell if your question are meant to be rhetorical, but I will err on the side of they are not and try to address them.
In my experience with the web or working in dynamic languages, the data you receive may come from multiple sources and/or not ideally normalized for proper processing. I found data like I presented above in things like service oriented architectures and even when compiling scientific data from different instrument sources (even worse for double blind studies or peer review aggregation). So while fp concepts in JS is easily explained with simple curried
add
functions onNumber
, I wanted to provide a case a bit closer to real life.Same answer as above.
A lot of this function is to normalize the input and separate the valid data points from the noise.
@evilsoft on twitter
Nice, just one of the effects of reading about them in textbooks without a professor to say the word out loud I guess.
It is not that Functional JS is weird, it is that working in a dynamic language presents some unique challenges that Haskell and Elm (and a few others) do not have to encounter.
This is where I think I may not have been clear on the intent. This is supposed to start down our path of understanding how to use ADTs in our functional JavaScript. I wanted to start by contrasting how different typical imperative JavaScript looks compared to the same functionality implemented code with ADTs in JavaScript. It seems that you were fixated on the MacGuffin that was used to drive the "plot" and be the actual focal point itself (other then to provide the contrast). So this seems to be where the failure in communication on my part lies.
I take every bit of criticism, good or bad to :heart: and use to improve my material in my video, live code and egghead lessons. So I appreciate you taking the time and passion to make sure the the content I provide is getting the message across.
I am going to make some tweaks to this posting to address a couple of the things you pointed out. And will defiantly keep some of these points in mind while I edit the other 7 posts in this series.
Thank You Again For Taking The Time,
evil.
EDIT: One thing I forgot to mention, for things like do not throw and just return an empty
Object
and things like that. When working in dynamic languages we tend to use a concept of Reasonable Defaults. So instead of just erroring out if life is not what we expect to be, we can signal that all is not well by returning an emptyObject
(in this case anyway) as anything that relies on this program would get theObject
it needs with the information that there is nothing to process.I was writing the comment while I was reading. Because I wanted to show you my thought process "in real time". If I were recording a screencast you would have heard me asking this question.
This way I can show where the friction occurs better.
Oh sorry about that.
Thanks so much for doing this, I find feedback like this very helpful!
Also did you happen to do any of the exercises? I am curious how they aided/hindered your understanding.
To be honest, I haven't.
The reason is... actually, it doesn't matter what the reason is.
I will do what I'm supposed to do.
I will finish what I started.
Now I will write my comment again in the same style. I have attached my gedit window with the above all other windows option. I will reread the article especially the functional part (because some of the functions I don't understand). I will narrate as I go along. I will call this "narration style".
I have to say that I'm not that proficient in JS. So correct me if I'm wrong. What I think I see here is destructuring. Pattern matching in other words. But on the right side there is a library so I don't really see how that works. I think I'll go right now and watch something on youtube to understand that part. I'll be as thorough as possible.
I opened youtube and searched es6 destructuring. I see a lot of videos. I'm going with the fun fun function video. If necessary I will narrate my process.
Here's an example:
let animal =
{ species: 'dog'
, weight: 23
, sound: 'woof'
}
// the destructuring
let { species, sound } = animal
// and from this I understand that the order doesn't matter.
Well, that's actually all I need at the moment. If order doesn't matter I can take out any function out of the library.
That's it. The video is 10 minutes long, I stopped at 1:27. Back to the tutorial.
At this moment I'm reading the requirements to really get them.
This part, was a bit hard to understand. Because of the use of the "double negative". I understand that it's not. But you can simplify it.
I'd go with:
When the data is an Array with at least one acceptable record, an empty Object will be returned.
Tell me if I've "refactored" your requirement correctly.
Oh, I'm looking the type signature:
// data :: [ * ]
the "*" is not a type variable. But a wildcard. It kinda expects something unpredictable inside an array.
You know I also look at this:
From these requirements, we can implement an imperative function that does the following:
Using a third-party library called crocks for some doing our type validation, we can provide an implementation like this:
And think that this could be written as comments in the code.
And that would be may be better, or not. Is there a way to find out?
At this point, I remembered that your purpose was to talk about ADT and monad. I went to the beginning of the article.
I'm not familiar with the word embelishment. I'm looking it up.
Embelishment is a decorative detail (elaboration, or addition).
I opened my editor to retype the example. I'm going back and forth between retyping and reading
I do it slowly so I understand everything thoroughly. I'm going back and forth. I read one implementation, then retype and think. Then I read another one, and then retype again.
Oh, now I've noticed you don't have semicolons in the code. That's good.
I need to download the 'crocks' library. To run the examples. I`ve just installed crocks.
npm install crocks -S
It says "All you need to do is run the following to save it as a dependency in your current project folder".
How do I save it as a dependancy? What is -S by the way?
Okay, I've got the imperative code well, I think. Now, to the functional part. Now, I think I should the functional part in a separate file. Or not?
Gosh, it's 1:43 in the morning. I'm getting so sleepy. I will continue tomorrow when I wake up.
Alright, I have woken up.
I want to make sure that I get the function composition in JS.
In Haskell we do it like this.
func1 . func2 . func3 x x
So in JS that would be
composeK(func1(Int), composeK(func2(Int), func3(Int, Int)))
This is hypothetical, but I think I get it right.
What I see that I don't understand is Maybe.of. After that I looked at the converge function. It takes a lot of functions as arguments. Right now I'm looking at the documentation on converge. The reason I'm diving deep is because I want to do the exercises.
Now I see this. I doesn't take all the arguments, so to speak. So there is partial application at play. Maybe.of takes data as an argument and then it all reduces to one value.
At this point I haven't scroll down, but I remember you have given some explanations below. Now it's time to read them.
The function Safe led me to Maybe in the library. I'm scrolling and see chain there as well. I looked at both don't really understand them.
Let's continue anyway.
So, I see the reference to the fluent api. Which doesn't tell me anything, I tried understand it, but decided that is just too much.
Conclusion: the reference to the fluent api is completely unnecessary and will create more confusion.
Now I will go through the exercises.
If the reduce works with numbers and these numbers will be reduced to a singe value I understand.
But when the reduce (or fold in my practice) works with different data types other than numbers I have problems.
Okay, I just thought it about.
Here we have an empty array as an accumulator, and we will push object that are satisfy a predicate.
Now should I write this predicate as a helper function?
Here's my little refactor:
`js
let result = records.reduce(filter_records(records), [])
const filter_records = records => {
return isObject(records) && isString(records.id)
}
`
I think it's not correct.
Will this work?
`js
let result = records.reduce(filter_records(sum, records), [])
const filter_records = (sum, records) => {
return sum + (isObject(records) && isString(records.id))
}
`
I unfortunately, cannot find out at the moment. Because, I have no idea of how to connect Crocks. It is installed, I've never used package.json, I've not used any build tools yet.
Here, I need some help.
2 - 5. I feel that lack of knowledge of JS is a little hindrance and that every time I do something I need to dig deeper. I wouldn't say I'm lazy. But in this particular instance I am. Maybe I will return at some point and do the exercises. I liked the "reduce" exercise.
Why did I wrote all those comments?
So good.
So there are a couple things you've shown me I need to do. First of I need to update:
To read more like:
Also good call on the needing of
crocks
for the POJ example. I need to break those out into functions like:Yeah people should not have to install crocks at this stage in the game.
How about
Arrays in JS can be of mixed type, so we typically denote that with
[ * ]
, as opposed to something like[ a ]
where we expect everything to be thata
. Well when our functions can work with mixed typedArray
s. In reality we want to strive for[ a ]
or even better something like[ Number ]
, but sometimes at the edge, we need the[ * ]
You got mostly the compose bits down, except you would just use
compose
,composeK
is for Kleisli arrows ( basically>=>
or fish in Haskell). Also you can use eithercompose
orcomposeK
like this:So this is more than just a filter, we are change types. we move from an
Array
to anObject
. So we want to reduce with{}
as our empty on the fold, then only add valid records keyed by their validid
to the accumulator. Does that make sense?Also just to help a fellow Haskeller out here is a quick JS -> Haskell key:
compose
->.
(butcompose
is n-Ary)composeK
->>=>
(butcomposeK
is n-Ary)[type].of
->pure
for Applicative Functor /return
for Monad[instance].chain
->bind
liftA2
->liftA2
converge
-> S' or Phoenix Combinator.I mostly don't know about them. So you are telling me new stuff that I should learn : ))) That's great. Thanks.
Do you think the concept "having the end mind" is applicable here?
and
So, basically you first show this at the start of the article (introduce the end goal):
And then you just show the imperative approach and then show how to refactor that approach into the functional one and what are the benefits.
Great article. Though I think someone unfamiliar with functional programming will get lost around the code block where you introduce:
Assign, Maybe, composeK, converge, isArray,
.isObject, isString, liftA2, mreduceMap, objOf,
prop, safe
There is a lot to digest in there. If you don't already have an understanding of every one of those functions, you risk losing the reader.
It could be helpful to have a step before this to explain these concepts. I think if someone already understands these, they will also already know what a
Monad
is too.Cheers!
Thanks so much for the feedback.
The intent for that is not to understand it, but compare how things look differently. Personally I like to show the end goal, at a glance. It is my hope by the end of this series they will be able to understand it.
Maybe I could lead with the paragraph saying it is okay to not understand this, just look at the style. Thoughts?
I guess it depends on who your target audience is. There are a lot of functions a non FPer has never been exposed to.
Reading your article taught me an important lesson:
I was reading your article in a linear fashion from to top to bottom. Instead of expecting the author to write in a style that I want. I can actually skim articles first and look for the inputs and outputs.
And then proceed to read the article in a linear fashion. This way I will have the bigger picture in mind and less friction when I start diving in.