If you are using functional programming, whether using a real functional language or any language that supports first-class function, you may have already used monads at some point. Most people just didn’t know they are monads. In this article, I am going to explain what is monad without using any mathematical theorem. Even more, I will demonstrate how to derive monad by only using plain JavaScript.
WARNING: This is an advanced topic. May not suit for beginner.DISCLAIMER: This article won’t provide the exact definition of monad. So don’t read it if you have an exam about monad.
I have been learning Haskell for about half year. I like to watch conference/meetup videos from youtube. One day, I watched a video called “Let’s be mainstream!”, in which the ELM creator, Evan Czaplicki, said “monad is callback”. I thought I understood what is monad that time, but I couldn’t figure out why Evan said that. Here is the link to that video, you can check after you finish this article if you want.
Several weeks passed, after reading lots of learning materials, especially after watched “Why Do Monads Matter” by Derek Wright. In this video, he demonstrate several different monads by trying to solve one problem: function composition.
The follow explanation basically use the same concept, but I’m going use JavaScript instead of Haskell.
So first, as a functional programmer, we all know function composition, which is very very easy, like this:
The composition function composeF is very easy, it takes two arguments f and g, and return a new function which return the result value after applying g then f. And we can use this function to combine mul3 and add1 to form the new function addOneThenMul3.
Now, assume we have a scenario:
What we have:
Next, we need to construct the function to fetch the “real content”.
The readFileSync is a synchronous IO function, which takes a path string as a input and return content of that given path in string. So we can reuse composeF to compose the function we want: readFileContentSync.
So far so good!
But what if the readFile function is asynchronous? If you are familiar with Node.js, then you probably have used callback to do flow control, which has a formal name: continuation-passing style or CPS.
Continuation-passing style_In functional programming, continuation-passing style ( CPS) is a style of programming in which control is passed…_en.wikipedia.org
Then the readFile function will look like this:
Now, here is a problem: We cannot use c_omposeF_ to do function composition. Because this time, the function doesn’t return anything.
But wait, we can still do function composition as long as:
All we need is a new composition function:
Notice that: I changed the arguments’ sequence: from (f, g) to (g, f).
You can see, c_omposeCPS_ will return a new function which will call g and in the callback of g, call f, and call cb with the the result value of f in the end.
Hooray! we can do function composition again!
Ok, let’s try something interesting.
First, we adjust the function signature of readFileCPS into readFileHOF:
const readFileHOF = path => cb => {readFileCPS(path, cb)}
HOF stands for Higher order Function. We just break one set of arguments into two set of arguments. Which makes readFileHOF takes a path argument and returns a function which take cb as input, and the real function body will execute after all arguments set.
And then, we need a new composition function.
Which is almost the same with composeCPS, not so different.
Secondly, we give a name to the function which take cb as argument, and put it into an object, like this:
Also, because we change the function signature again, so we need to adjust the composition function:
Which is actually, again, not so different from composeHOF, right?
We hate Repeat Yourself. So let’s create a helper function:
const createExecObj = exec => ({exec})
This function returns an object with exec field, which is also a function. Let’s call the “object with exec field” ExecObj
createExecObj also make readFileEXEC two line shorter:
const readFileEXEC2 = path => {return createExecObj(cb => {readFileCPS(path, cb)})}
After this change, you can see readFileEXE2 takes a path and return an exec object. Other than that, everything is the same.
Next step, we are going to have a big change, so BE AWARE!
So far, the both arguments of composition function are functions. Let’s change the first argument to something else: the exec object!
We rename the compose to bind. This bind function will return a new exec object. If anyone call exec of this object, then it will do the same thing to execObj and then in the callback, it uses f. Notice that line 4 of this gist is the same as the line 6 of composeEXEC.
Then the magic start:
const readFile2EXEC2 = bindExec(readFileEXEC2('./file1'),readFileEXEC2)readFile2EXEC2.exec(result => console.log(result))
Not clear enough? Let’s remove the constant:
bindExec(readFileEXEC2('./file1'),readFileEXEC2).exec(result => console.log(result))
Still don’t get it? Well, we’re almost there, be patient:
Let’s try to “inline” the bindExec function to the exec object, like this:
Please compare this with bindExec. The return object is the exec object, so we simply replace the execObj with this. Of course, because of the same reason, we don’t need first argument (line 3)
So how to use it?
readFileEXEC2('./file1').bind(readFileEXEC2).exec(result => console.log(result))
Do you see that? Something very familiar? No?
Ok! Let’s do some rename:
readFileAsync('./file1').then(readFileAsync).done(result => console.log(result))
Oh my god! This is a promise!!!(Not exactly. Because promise will execute whether you call done.)
Or
Oh my god! This is Rx!!!(Yes! almost! Of course we still need to add error handling and other stuff)
The answer is: from composeCPS until here. They are all monads!!! Specifically:
Well, first, maybe you’re asking a wrong question. Instead of asking “Is it a monad”, you should ask “Can it be a monad”.
For example:
Check with those properties:
So yes, array can be a monad.
No!
For example, lots of developers using array, promise and Rx without understand monad. It’s OK. So did I. I used Rx for a long time without really understand monad. But still, it is helpful if you want to create a tool like Rx. (Well, we just did it :p)
So “Monad is Callback“?
Yes! According to property 3.
Then “Callback is Monad”?
Not yet! Only if there is a bind function defined.
Thanks to Tom Chen, Guan-Yu Pan, Wu, Ching Ting to for reviewing this article.
UPDATE: fix wrong wording: passion => patient, thanks to Ian Hofmann-HicksUPDATE: rename readFileFluent, thanks to wkliang