© Anto Aravinth, Srikanth Machiraju 2018
Anto Aravinth and Srikanth MachirajuBeginning Functional JavaScripthttps://doi.org/10.1007/978-1-4842-4087-8_9

9. Monads in Depth

Anto Aravinth1  and Srikanth Machiraju2
(1)
Chennai, Tamil Nadu, India
(2)
Hyderabad, Andhra Pradesh, India
 

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.

Note

The chapter examples and library source code are in branch chap09. The repo’s URL is https://github.com/antsmartian/functional-es8.git

Once you check out the code, please check out branch chap09:

...

git checkout -b chap09 origin/chap09

...

For running the codes, as before run:

...

npm run playground

...

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.

Note

You might be wondering why we are not using the Either functor for the current problem, as MayBe has a few drawbacks of not capturing the error when branching out as we saw in the previous chapter. That’s true, but the reason we 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:
  1. 1.

    For searching specific posts and 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 the result shown in Listing 9-1.

     
{ kind: 'Listing',
  data:
   { facets: {},
     modhash: ",
     children:
      [ [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        [Object],
        . . .
        [Object],
        [Object] ],
     after: 't3_terth',
     before: null } }
Listing 9-1

Structure 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.
  1. 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-2

searchReddit Function Definition

Now we’ll walk through the code in steps.
  1. 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. 2.

    Once the response is a success, we are returning back the value.

     
  3. 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'] }
    }
    return response
}
Listing 9-3

getComments Function Definition

The getComments implementation is very similar to our searchReddit. Let’s walk through the steps and see what getComments does.
  1. 1.
    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/.json

    getComments then will fire an 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 . 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")))
                    }
               }));
   return ans;
}
Listing 9-4

mergeViaMayBe 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] } ] }

Note

For better clarity we have reduced the number of results in the output of this 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, we display only minimal output in the book. Note, though, that the source code example does call and print all 25 results.

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")))
. . .

Note

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.

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-5

join 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)
=> MayBe { value: 9 }

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-6

mergeViaMayBe 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-7

chain 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()
                    }
               }))
   return ans;
}
Listing 9-8

mergeViaMayBe 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
                       })
                    }
               }))
   return ans;
}
Listing 9-9

Making 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.

Note

You might be 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 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.