Note
The chapter examples and library source code are in branch chap09. The repo’s URL is: https://github.com/antoaravinth/functional-es6.git
Once checkout the code, please checkout branch chap09:
...
git checkout -b chap09 origin/chap09
...
For running the codes, as before run:
...
npm run playground
...
In the previous chapter we have seen what Functors are and how they are useful to us. In this chapter we are going to continue with Functors. We will learn about a new functor called Monads . Don’t be afraid with the terms: the concepts are easy.
We are going to start with a problem of retrieving and displaying the reddit comments for our search query. Initially we are going to use Functors, especially the MayBe functor, to solve this problem. But while we solve the problem, we are going to encounter a few issues with the MayBe functor. Then we will be moving ahead to create a special type of functor called Monad.
Getting Reddit Comments for Our Search Query
We have been using reddit API starting from the previous chapter. In this section, too, we will be using the reddit API for searching the posts with our query and getting the list of comments for each of the search results. We are going to use MayBe for this problem; as we have seen in the previous chapter, MayBe allows us to focus on the problem, without worrying about those pesky null/undefined values.
You might be wondering why not the Either functor for the current problem, as MayBe has a few drawbacks of not capturing the error when branching out as we have seen in the previous chapter. That’s true, but the reason I have chosen MayBe is mainly to keep things simple. As you see, we will be extending the same idea to Either as well!
The Problem
Before we begin implementing the solution, let’s look at the problem and its associated Reddit API endpoints. The problem contains two steps:
For searching a specific posts/comments/ we need to hit the Reddit API endpoint :
https://www.reddit.com/search.json?q=<SEARCH_STRING >
and pass along the SEARCH_STRING. For example, if we search for the string functional programming like this:
https://www.reddit.com/search.json?q=functional%20programming
we get back:
Listing 9-1. Structure of Reddit Response
{ kind: 'Listing',data:{ facets: {},modhash: '',children:[ [Object],[Object],[Object],[Object],[Object],[Object],. . .[Object],[Object] ],after: 't3_terth',before: null } }and each children object looks like this:
{ kind: 't3',data:{ contest_mode: false,banned_by: null,domain: 'self.compsci',. . .downs: 0,mod_reports: [],archived: true,media_embed: {},is_self: true,hide_score: false,permalink: '/r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/?ref=search_posts',locked: false,stickied: false,. . .visited: false,num_reports: null,ups: 134 } }where these objects specify the results that are matching our search query.
Once we have the search result, we need to get each search result’s comments. How do we do it? As mentioned in the previous point, each children object is our search result. These objects have a field called permalink, which looks like :
permalink: '/r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/?ref=search_posts',we need to navigate to the above URL:
GET: https://www.reddit.com//r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/.jsonwhich is going to return the array of comments like the following:
[Object,Object,..,Object]where each Object gives the information about comments.
Once we get the comments object, we need to merge the result with title and return as a new Object:
{title : Functional programming in plain English,comments : [Object,Object,..,Object]}where title is the title we get from the first step. Now with our understanding of the problem, let’s go and implement the logic.
Implementation of the First Step
Let’s implement the solution step by step. In this section, we’ll implement the solution for the first step. The first step involves hitting the Reddit search API endpoint along with our search query. Since we need to fire the HTTP GET call, we will be requiring the sync-request module that we used in the previous chapter.
Let’s pull out the module by and hold it in a variable for future use:
let request = require('sync-request');Now with the request function, we could fire HTTP GET call to our Reddit Search API endpoint. Let’s wrap the search steps in a specific function, which we call searchReddit:
Listing 9-2. searchReddit function definition
let searchReddit = (search) => {let responsetry{response = JSON.parse(request('GET',"https://www.reddit.com/search.json?q=" + encodeURI(search)).getBody('utf8'))}catch(err) {response = { message: "Something went wrong" , errorCode: err['statusCode'] }}return response}
Now we’ll walk down the code in steps:
We are firing the search request to the URL endpoint https://www.reddit.com/search.json?q = as shown here:
response = JSON.parse(request('GET',"https://www.reddit.com/search.json?q=" + encodeURI(search)).getBody('utf8'))Note that we are using the encodeURI method for escaping special characters in our search string.
Once the response is a success, we are returning back the value.
In case of error, we are catching it in a catch block and getting the error code and returning the error response like this:
. . .catch(err) {response = { message: "Something went wrong" , errorCode: err['statusCode'] }}. . .With our little function in place, we go ahead and test it:
searchReddit("Functional Programming")which will return the result:
{ kind: 'Listing',data:{ facets: {},modhash: '',children:[ [Object],[Object],[Object],[Object],[Object],[Object],[Object],[Object],. . .after: 't3_terth',before: null } }
That’s perfect! We are done with step 1! Let’s go and implement step 2.
Implementing the decond Step For each search children object, we need to get its permalink value to get the list of comments. We can write a separate method for getting s list of comments for the given URL. We call this method getComments. The implementation of getComments is simple, which looks like the following:
Listing 9-3. getComments function definition
let getComments = (link) => {let responsetry {response = JSON.parse(request('GET',"https://www.reddit.com/" + link).getBody('utf8'))} catch(err) {response = { message: "Something went wrong" , errorCode: err['statusCode'] }}return response}
getComments implementation is very similar to our searchReddit. Let’s walk down in steps and see what getComments does:
It fires the HTTP GET call for the given link value. For example, if the link value is passed as:
r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.jsongetComments then will fire a HTTP GET call to the URL:
https://www.reddit.com/r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.json
which is going to return the array of comments. As before, we are a bit defensive here and catching any errors within the getComments method in our favorite catch block. And finally we are returning back the response.
Quickly we’ll test our getComments, by passing the below link value:
r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.jsongetComments('r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.json')
for the above call we get back:
[ { kind: 'Listing',data: { modhash: '', children: [Object], after: null, before: null } },{ kind: 'Listing',data: { modhash: '', children: [Object], after: null, before: null } } ]
the result. Now with both APIs ready, it’s time to go and merge these results.
Merging Reddit Calls. Now we have defined two functions, namely, searchReddit and getComments (Listing 9-2 and Listing 9-3 respectively), which does it tasks and return the response as seen in the previous sections. In this section, let’s write a higher-level function, which takes up the search text and use these two functions to achieve our end goal.
We’ll call the function we create as mergeViaMayBeand its implementation looks like the following:
Listing 9-4. mergeViaMayBe function definition
let mergeViaMayBe = (searchText) => {let redditMayBe = MayBe.of(searchReddit(searchText))let ans = redditMayBe.map((arr) => arr['data']).map((arr) => arr['children']).map((arr) => arrayUtils.map(arr,(x) => {return {title : x['data'].title,permalink : x['data'].permalink}})).map((obj) => arrayUtils.map(obj, (x) => {return {title: x.title,comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json")))}}));return ans;}
Let’s quickly check our function by passing the search text functional programming:
mergeViaMayBe('functional programming')the above call will give the result:
MayBe {value:[ { title: 'ELI5: what is functional programming and how is it different from OOP',comments: [Object] },{ title: 'ELI5 why functional programming seems to be "on the rise" and how it differs from OOP',comments: [Object] } ] }
For better clarity I have reduced the number of results in the output of the above call. The default call will give back 25 results, which will take a couple of pages to put in the output of mergeViaMayBe. From here on, I will be displaying only minimal output in the book. Kindly note that the source code example does call and print all of the 25 results.
Great! Now let’s step back and understand in detail what mergeViaMayBe function does.
The function first calls the searchReddit with searchText value. The result of the call is wrapped in MayBe:
let redditMayBe = MayBe.of(searchReddit(searchText))and once the result is wrapped inside a MayBe type, we are free to map over it as you can see in the code.
To remind us of the search query (which our searchReddit will call), it will send back the result in the following structure:
{ kind: 'Listing',data:{ facets: {},modhash: '',children:[ [Object],[Object],[Object],[Object],[Object],[Object],. . .[Object],[Object] ],after: 't3_terth',before: null } }
In order to get the permalink (which is in our children object), we need to navigate to data.children. This is exactly demonstrated in the code:
redditMayBe.map((arr) => arr['data']).map((arr) => arr['children'])
Now we got the handle to children array. Remember that each children has an object with the following structure:
{ kind: 't3',data:{ contest_mode: false,banned_by: null,domain: 'self.compsci',. . .permalink: '/r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/?ref=search_posts',locked: false,stickied: false,. . .visited: false,num_reports: null,ups: 134 } }
We need to get only title and permalink out of it; since it’s an array, we run Array's map function over it:
.map((arr) => arrayUtils.map(arr,(x) => {return {title : x['data'].title,permalink : x['data'].permalink}}))
Now we have both title and permalink, our last step is to take permalink and pass it to our getComments function, which will fetch the list of comments for the passed value. This is seen here in the code:
.map((obj) => arrayUtils.map(obj, (x) => {return {title: x.title,comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json")))}}));
Since the call of getComments can get an error value, we are wrapping it again inside a MayBe:
. . .comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))). . .
Note
That we are replacing the permalink value ?ref=search_posts with .json as search results append the value ?ref=search_posts, which is not the correct format for the getComments API call.
That's it!
Throughout the full process we haven't come outside our MayBe type. We run our all map function happily on our MayBe type without worrying about it too much! We have solved our problem so elegantly with MayBe, didn’t we? There is a slight problem with our MayBe functor that is used this way. Let’s talk about it in next section.
Problem of So Many maps
If you count the number of map calls on our MayBe in our mergeViaMayBe function, its 4! You might be thinking what is the big deal about it? Who cares about the number of map calls! Do we?
Let’s try to understand the problem of many chained map calls like in mergeViaMayBe. Now imagine we want to get a comments array that is returned from mergeViaMayBe.
We’ll pass our search text functional programming in our mergeViaMayBe function:
let answer = mergeViaMayBe("functional programming")after the call answer:
MayBe {value:[ { title: 'ELI5: what is functional programming and how is it different from OOP',comments: [Object] },{ title: 'ELI5 why functional programming seems to be "on the rise" and how it differs from OOP',comments: [Object] } ] }
Now let’s get the comments object for processing. Since the return value is MayBe, we can map over it:
answer.map((result) => {//process result.})
The result (which is the value of MayBe) is an array that has title and comments, so let’s map over it using our Array's map:
answer.map((result) => {arrayUtils.map(result,(mergeResults) => {//mergeResults})})
Each mergeResults is an object, which has title and comments. Remember that comments are also a MayBe! So in order to get comments, we need to map over our comments:
answer.map((result) => {arrayUtils.map(result,(mergeResults) => {mergeResults.comments.map(comment => {//finally got the comment object!})})})
It looks like we have done more work to get the list of comments! Imagine someone is using our mergeViaMayBe API to get the comments list. They will be really irritated to get back the result using nested maps as you can see above. Can we make our mergeViaMayBe better? Yes we can – meet Monads!
Solving the Problem via join
We saw in previous sections how deep we have to go inside our MayBe to get back our desired results! Writing such API's is not going to help us definitely but rather irritate other developers working on it! In order to solve these deep-nested issues, let’s add join to the MayBe functor.
join Implementation
Let’s start implementing the join function. The join function is simple and looks like the following:
Listing 9-5. join function definition
MayBe.prototype.join = function() {return this.isNothing() ? MayBe.of(null) : this.value;}
join is very simple and it simply returns the value inside our container (if there are values); if not, returning MayBe.of(null). join is simple, but it helps us to unwrap the nested MayBe's:
let joinExample = MayBe.of(MayBe.of(5))=> MayBe { value: MayBe { value: 5 } }joinExample.join()=> MayBe { value: 5 }
As shown in the above example, it unwraps the nested structure into a single level! Imagine we want to add 4 to our value in joinExample MayBe. Let’s give it a try:
joinExample.map((outsideMayBe) => {return outsideMayBe.map((value) => value + 4)})
The above code gives back:
MayBe { value: MayBe { value: 9 } }Even though the value is correct, we have mapped twice to get the result. Again the result that we got ends up in a nested structure! Now let’s do the same via join:
joinExample.join().map((v) => v + 4)=> MayBe { value: 9 }
Wow, the above code is just elegant! The call to join returns the inside MayBe, which has the value of 5; once we have that, we are running over it via map and thenadd the value 4. Now the resulting value is in a flatten structure MayBe { value: 9 }.
Now with join in place, let’s go and try to level our nested structure returned by mergeViaMayBe. We’ll change the code to the following:
Listing 9-6. mergeViaMayBe using join
let mergeViaJoin = (searchText) => {let redditMayBe = MayBe.of(searchReddit(searchText))let ans = redditMayBe.map((arr) => arr['data']).map((arr) => arr['children']).map((arr) => arrayUtils.map(arr,(x) => {return {title : x['data'].title,permalink : x['data'].permalink}})).map((obj) => arrayUtils.map(obj, (x) => {return {title: x.title,comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).join()}})).join()return ans;}
As you can see, we have just added two joins in our code. One is on the comments section, where we create a nested MayBe; and another one is right after our all map operation.
Now with mergeViaJoinin place, let’s go and implement the same logic of getting the comments array out of the result.
First let’s quickly see the response returned by mergeViaJoin:
mergeViaJoin("functional programming")which is going to return:
[ { title: 'ELI5: what is functional programming and how is it different from OOP',comments: [ [Object], [Object] ] },{ title: 'ELI5 why functional programming seems to be "on the rise" and how it differs from OOP',comments: [ [Object], [Object] ] } ]
Compare the above result with our old mergeViaMayBe:
MayBe {value:[ { title: 'ELI5: what is functional programming and how is it different from OOP',comments: [Object] },{ title: 'ELI5 why functional programming seems to be "on the rise" and how it differs from OOP',comments: [Object] } ] }
As you can see, join has taken out the MayBe’s value and sent it back. Now lets see how to use the comments array for our processing task. Since the value returned from mergeViaJoin is an array, we can map over it using our Arrays map:
arrayUtils.map(result, mergeResult => {//mergeResult})
Now each mergeResult variable directly points to the object that has title and comments. Note that we have called join in our MayBe call of getComments, so the comments object is just a simple array! With that in mind, to get the list of comments from the iteration, we just need to call mergeResult.comments:
arrayUtils.map(result,mergeResult => {//mergeResult.comments has the comments array!})
This looks promising, as we have gotten the full benefit of our MayBe and also a good data structure to return the result, which are easy for processing!
chain Implementation
Have a look at the code in listing 9-6. As you can guess, we need to call join always after map. Let’s wrap this logic inside a method called chain:
Listing 9-7. chain function definition
MayBe.prototype.chain = function(f){return this.map(f).join()}
Once chain is in place, we can make our merge function logic to looks like this:
Listing 9-8. mergeViaMayBe using chain
let mergeViaChain = (searchText) => {let redditMayBe = MayBe.of(searchReddit(searchText))let ans = redditMayBe.map((arr) => arr['data']).map((arr) => arr['children']).map((arr) => arrayUtils.map(arr,(x) => {return {title : x['data'].title,permalink : x['data'].permalink}})).chain((obj) => arrayUtils.map(obj, (x) => {return {title: x.title,comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).join()}}))return ans;}
The output is going to be exactly the same via chain too! Go and play around with above function! In fact, with chain in place, we can move the logic of counting the number of comments to an in-place operation:
Listing 9-9. Making improvements on mergeViaChain
let mergeViaChain = (searchText) => {let redditMayBe = MayBe.of(searchReddit(searchText))let ans = redditMayBe.map((arr) => arr['data']).map((arr) => arr['children']).map((arr) => arrayUtils.map(arr,(x) => {return {title : x['data'].title,permalink : x['data'].permalink}})).chain((obj) => arrayUtils.map(obj, (x) => {return {title: x.title,comments: MayBe.of(getComments(x.permalink.replace("?ref=search_posts",".json"))).chain(x => {return x.length})}}))return ans;}
Now calling the above code:
mergeViaChain("functional programming")will return the following:
[ { title: 'ELI5: what is functional programming and how is it different from OOP',comments: 2 },{ title: 'ELI5 why functional programming seems to be "on the rise" and how it differs from OOP',comments: 2 } ]
Bingo! We won! The solution looks so elegant! But still we haven’t seen a Monad, have we?
So What Is a Monad?
You might be wondering why we started the chapter with a promise of teaching you a Monad! But still now we haven’t defined what a Monad is. I'm sorry for not defining the Monad, but you have already seen it in action. (What?!!)
Yes, Monad is a functor that has a chain method ! Yeah, that’s it, that’s what a Monad is! As you have already seen, we have extended our favorite MayBe functor to add a chain (and of course a join function) to make it a Monad!
We started with an example of a functor to solve an ongoing problem and ended up solving the problem using a Monad without even being aware of using it! That’s intentional from my side as I wanted to see the intuition behind Monad (the problem it solves in hand with a functor)! I could have started with a simple definition of Monad, but that directly shows what a Monad is, but it won’t show why a Monad!
You might be in confused thinking about whether MayBe is a Monad or a Functor! Don't get confused: MayBe with only of and map is a Functor. A functor with chain is a Monad!
Summary
In this chapter we have seen a new Functor type called Monad. We discussed the problem of how repetitive maps will cause nested values, which become tough to handle later! We introduced a new function called chain, which helps to flatten the MayBe data. We saw that a pointed functor with a chain is called a Monad. In the current chapter, we were using a third- party library to create AJAX calls. In the next chapter, we will be seeing a new way to think of Asynchronous calls.