In the previous chapter we saw what functors are and how they are useful to us. In this chapter we are going to continue with functors, learning about a new functor called a monad. Don’t be afraid of the terms; the concepts are easy to understand.
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. As we solve the problem, though, 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 a monad.
Getting Reddit Comments for Our Search Query
We have been using Reddit API starting with the previous chapter. In this section, we use the same 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 saw in the previous chapter, MayBe allows us to focus on the problem without worrying about null/undefined values.
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:
{ kind: 'Listing',
data:
{ facets: {},
modhash: ",
children:
[ [Object],
[Object],
[Object],
[Object],
[Object],
[Object],
. . .
[Object],
[Object] ],
after: 't3_terth',
before: null } }
Listing 9-1Structure of Reddit Response
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 } }
These objects specify the results that are matching our search query.
- 2.
Once we have the search result, we need to get each search result’s comments. How do we do that? As mentioned in the previous point, each children object is our search result. These objects have a field called permalink, which looks like this:
permalink: '/r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/?ref=search_posts',
We need to navigate to the preceding URL:
GET: https://www.reddit.com//r/compsci/comments/3mecup/eli5_what_is_functional_programming_and_how_is_it/.json
That 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 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 implement the logic.
Implementation of the First Step
In this section, we implement the solution for the first step, which involves firing a request to the Reddit search API endpoint along with our search query. Because 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 and hold it in a variable for future use:
let request = require('sync-request');
Now with the
request function, we could fire the
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).
let searchReddit = (search) => {
let response
try{
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
}
Listing 9-2searchReddit Function Definition
Now we’ll walk through the code in steps.
- 1.
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.
- 2.
Once the response is a success, we are returning back the value.
- 3.
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")
This will return the following 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 implement Step 2.
Implementing the second 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 a list of comments for the given URL. We call this method
getComments
. The implementation of
getComments is simple, as shown in Listing
9-3.
let getComments = (link) => {
let response
try {
response = JSON.parse(request('GET',"https://www.reddit.com/" + link).getBody('utf8'))
} catch(err) {
response = { message: "Something went wrong" , errorCode: err['statusCode'] }
}
Listing 9-3getComments Function Definition
The
getComments implementation is very similar to our
searchReddit. Let’s walk through the steps and see what
getComments does.
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
. Finally, we are returning back the response.
Quickly we’ll test our
getComments by passing the following
link value
:
r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.json
getComments('r/IAmA/comments/3wyb3m/we_are_the_team_working_on_react_native_ask_us/.json')
For this call we get back this result:
[ { kind: 'Listing',
data: { modhash: ", children: [Object], after: null, before: null } },
{ kind: 'Listing',
data: { modhash: ", children: [Object], after: null, before: null } } ]
Now with both APIs ready, it’s time to merge these results.
Merging Reddit Calls
Now we have defined two functions, namely,
searchReddit
and getComments
(Listing 9-2 and Listing 9-3, respectively), that perform their tasks and return the responses 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
mergeViaMayBe and its implementation looks like Listing
9-4.
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")))
}
}));
Listing 9-4mergeViaMayBe Function Definition
Let’s quickly check our function by passing the search text
functional programming
:
mergeViaMayBe('functional programming')
That call will give this 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] } ] }
Now let’s step back and understand in detail what the
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))
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 } }
To get the
permalink
(which is in our
children object), we need to navigate to
data.children. This is demonstrated in the code:
redditMayBe
.map((arr) => arr['data'])
.map((arr) => arr['children'])
Now that we have a handle on a
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; because 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 that 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")))
}
}));
Because 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")))
. . .
Throughout the full process we haven’t come outside our MayBe type
. We run our all map functions happily on our MayBe type without worrying about it too much. We solved our problem so elegantly with MayBe, didn’t we? There is a slight problem with our MayBe functor that is used this way, though. Let’s talk about it in the next section.
Problem of Nested/Many maps
If you count the number of map
calls on our MayBe in our mergeViaMayBe function, it is four. You might be wondering why we care about the number of map calls.
Let’s try to understand the problem of many
chained map calls like in
mergeViaMayBe. 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. Because 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. To get
comments, therefore, 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 already shown. 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 APIs is not going to help us, but rather will irritate other developers working on it. 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 Listing
9-5.
MayBe.prototype.join = function() {
return this.isNothing() ? MayBe.of(null) : this.value;
}
Listing 9-5join Function Definition
join is very simple and it simply returns the value inside our container (if there are values); if not, it returns
MayBe.of(null). join is simple, but it helps us to unwrap the nested
MayBes:
let joinExample = MayBe.of(MayBe.of(5))
=> MayBe { value: MayBe { value: 5 } }
joinExample.join()
=> MayBe { value: 5 }
As shown in this 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)
})
This code returns the following:
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)
That code is simply 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 then add the value 4. Now the resulting value is in a flatten structure MayBe { value: 9 }.
Now with
join in place, let’s try to level the nested structure returned by
mergeViaMayBe. We’ll change the code to Listing
9-6.
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;
}
Listing 9-6mergeViaMayBe Using join
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
mergeViaJoin in place, let’s implement the same logic of getting the
comments array out of the result. First let’s quickly look at the response returned by
mergeViaJoin:
mergeViaJoin("functional programming")
That is going to return the following:
[ { 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 that 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 let’s see how to use the
comments array for our processing task. Because 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 results, 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
, as shown in Listing
9-7.
MayBe.prototype.chain = function(f){
return this.map(f).join()
}
Listing 9-7chain Function Definition
Once
chain is in place, we can make our merge function logic look like Listing
9-8.
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()
}
}))
Listing 9-8mergeViaMayBe Using chain
The output is going to be exactly the same via
chain, too. Play around with this function. In fact, with
chain in place, we can move the logic of counting the number of
comments to an in-place operation, as shown in Listing
9-9.
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
})
}
}))
Listing 9-9Making Improvements on
mergeViaChain
Now calling this 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 } ]
The solution looks so elegant, but we still haven’t seen a monad, have we?
What Is a Monad?
You might be wondering why we started the chapter with a promise of teaching you about a monad, but still haven’t defined what a monad is. We’re sorry for not defining the monad, but you have already seen it in action. (What?)
Yes, a monad is a functor that has a chain method; 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 our side as we wanted to see the intuition behind monad (the problem it solves with a functor). We could have started with a simple definition of monad, but although that shows what a monad is, it won’t show why a monad should be used.
Summary
In this chapter we have seen a new functor type called a monad. We discussed the problem of how repetitive maps will cause nested values, which become difficult 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 this 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.