Friday, March 29, 2024

Advanced Functional Programming: Monads in JavaScript

Have you noticed that, more and more frequently, the term Functional Programming is being used by the community?

In this article, we will show the more technical details for those who want to deepen their understanding of the subject.

So, keep reading this post to learn:

  1. Composition of functions
  2. Pointfree functions
  3. Using pipes with Pareto.js
  4. What are Monads

Function Composition

If you already understand the basics of Functional Programming, you have realized that one of the main goals is to build small (and pure) functions, so that we can compose them and develop other larger and more complex functions.

With that in mind, let’s build a functional solution to generate a simple slug of a string:

slug('New Blog Post') // new-blog-post

That is, we receive a string as a parameter and return a transformation of this string: 1) we apply a lowercase and 2) we substitute spaces by dashes.

The simple code below solves this problem:

const toLowerCase = s => s.toLowerCase()
const replaceSpaces = s => s.replace(/s+/g, '-')
const slug = title => replaceSpaces(toLowerCase(title))

slug('New Blog Post') // new-blog-post

The slug function, which is the main function, receives a string title as a parameter, then applies the toLowerCase function in title and returns this value so that the replaceSpaces function returns the new transformed string.

Thus:

slug('New Blog Post');
 1. toLowerCase('New Blog Post'); 
 2. replaceSpaces('new blog post');
 3. 'novo-post-no-blog'

In short, the slug function is a composition of functions.

Although we are already on a more functional path, we still have some problems with the slug function. Readability is one of them:

const slug = title => replaceSpaces(toLowerCase(title))

We read the function from left to right:

  1. replaceSpaces
  2. toLowerCase

But the slug function, as I showed earlier, applies these two functions in the opposite order in the way we read. So, how to solve this problem?

Pipes

Composition of functions is so common when using the functional paradigm, that a common standard for doing this composition is to use pipes.

JavaScript does not come with a pipe function in the language itself, so we’ll use a functional lib for this. We could use Ramda.js without any problem, but in this example, I will use Pareto.js, which is very similar to Ramda.js. The difference is that it is lightweight and more modern.

With the pipe function then, the problem would be solved this way:

import { pipe } from 'paretojs'

const toLowerCase = s => s.toLowerCase()
const replaceSpaces = s => s.replace(/s+/g, '-')

const slug = pipe(toLowerCase, replaceSpaces)

slug('New Blog Post') // new-blog-post

In this way, we reached the first goal, which was to improve the reading order of the function that is now more natural: from left to right.

In addition, we also get another more subtle advantage. Read the two versions of the slug function again:

// old version
const slug = title => replaceSpaces(toLowerCase(title))

// new version
const slug = pipe(toLowerCase, replaceSpaces)

The new version does not contain any reference to the title parameter that will be passed. This has a name: pointfree function.

With this, we do not get so coupled to a specific parameter name and we have a more flexible function to possible changes and easier to read.

Null

Our slug function works great, there’s only one problem — in the real world, we eventually pass null as a parameter. In that case, the function would break and we would not even know what happened.

We can solve this issue in a simple way:

const toLowerCase = s => {
 if (s !== null) {
  return s.toLowerCase()
 }
}

const replaceSpaces = s => {
 if (s !== null) {
  return s.replace(/s+/g, '-')
 }
}

But this solution is not very scalable: we need to put the null check on all our functions, which should be simple and focused on solving only one problem.

It would be much simpler to centralize this check in one place.

Monads

A Monad, in short, is simply a wrapper of any value. The Maybe function below does just that:

function Maybe(value) {
 return {
  value: value
 }
}

const maybeString = Maybe('New Blog Post')
maybeString.value // 'New Blog Post'

const maybeNull = Maybe(null)
maybeNull.value // null

With this, we already have a wrapper, but still some details are missing. First, let’s do a refactoring in the Maybe function, to take advantage of some benefits of ES6.

Let’s transform it then:

// ES5
function Maybe(value) {
 return {
  value: value
 }
}

To this:

// ES6
const Maybe = value => ({
 value
})

Since our main goal with Monads is to have a guarantee against null, let’s add an isNothing function that does this check:

const Maybe = value => ({
 value,
 isNothing() {
  return (this.value === null)
 }
})

const maybeString = Maybe('New Blog Post')
maybeString.isNothing() // false

const maybeNull = Maybe(null)
maybeString.isNothing() // true

To finalize our Monad, we need a new function that can:

  1. Apply another function to the value of the wrapper, if this value exists
  2. Do nothing if the value is null
const Maybe = value => ({
 value,
 isNothing() {
  return (this.value === null)
 },
 map(fn) {
  if (this.isNothing()) {
   return Maybe(null)
  }
  return Maybe(fn(this.value))
 }
})

const toLowerCase = s => s.toLowerCase()

const maybeString = Maybe('New Blog Post')
maybeString.map(toLowerCase) // Maybe {value: 'new blog post'}

const maybeNull = Maybe(null)
maybeNull.map(toLowerCase) // Maybe {value: null}

Now that we have the finished version of Maybe Monad, we just need to update the initial version of the slug function:

import { pipe } from 'paretojs'

const toLowerCase = maybeStr => maybeStr.map(s => s.toLowerCase())
const replaceSpaces = maybeStr => maybeStr.map(s => s.replace(/s+/g, '-'))

const slug = pipe(toLowerCase, replaceSpaces)

const maybeTitle = Maybe('New Blog Post')
slug(maybeTitle) // Maybe {value: 'New Blog Post'}

const maybeNull = Maybe(null)
slug(maybeNull) // Maybe {value: null}

Once this is done, we are able to compose functions in a functional way and protect ourselves from null.

Remember that we need to do all this because JavaScript is not an 100% functional language. Elm for example, already comes with everything we did in this post built into the language itself.

 	const toLowerCase = s => s.toLowerCase()
 	const replaceSpaces = s => s.replace(/s+/g, '-')
 	const slug = title => replaceSpaces(toLowerCase(title))

About the Author

Diogo Souza works as a Java Developer at PagSeguro and has worked for companies such as Indra Company, Atlantic Institute and Ebix LA. He is also an Android trainer, speaker at events on Java and mobile world.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured