๐Ÿงฉ๐Ÿš€๐Ÿ”ฎ Mastering Functional JavaScript: Composition, Functors, and Monads Unveiled

2022-06-30

Introduction

๐Ÿš€ Today, we're going to talk about functional programming, which has become very popular among JavaScript developers. Simply put, functional programming is a paradigm where applications are composed using pure functions, avoiding shared mutable state and side-effects.

Functional Programming Paradigm

JavaScript has the most important features needed for functional programming:

All three listed properties come from lambda calculus, which we will touch on in the next post, how it started and where the idea of lambda calculus came from. Functional programming is entirely based on the lambda calculus.

Functional programming is usually more declarative than imperative, meaning that we express what to do rather than how to do it. Functional code is more concise, more predictable, we are almost sure what the result will be and accordingly it becomes easier to test than arbitrary imperative or object-oriented code.

Letโ€™s explain all the basic and significant concepts we have in functional programming.

Composition ๐Ÿงฉ

Letโ€™s consider an example of composition, letโ€™s say we have 2 functions and we want to compose this functions.

composition.js
	const f = n => n + 5;
	const g = n => n * 2;

Let's write a composition of this functions:

composition.js
	const compose = x => f(g(x));

We know from algebra that (f โ—ฆ g)(x) = f (g(x)), let's rewrite the compose function:

composition.js
	const compose = (f,g) => x => f(g(x))

๐Ÿ”ฎ That's it. Now let us describe this union of functions in an even more general way, directly getting an array of functions as an argument.

compose.js
	const compose = (...fns) => x => 
	   fns.reduce(
		(currentValue, currentFunction) => currentFunction(currentValue)
	   ,x);

No, let's play with this compose and compose some functions.

composition-example.js
	const toUpper = str => str.toUpperCase();
	const exclaim = str => str + '!';
	const firstLetter = xs => xs[0];
 
	const loud  = compose(toUpper, firstLetter);
	const shout  = compose(loud,exclaim);
 
	console.log(shout("example"))// returns E!

I've created some functions and make a compositons, loud is composition of two functions and shout is composition of loud(which is a composition itself) and exclaim functions.

Composition is associative.In mathematics and computer science, a function or an operation is said to be associative if the way you group operations does not change the result. This means that if you have three or more items, it doesn't matter how they are grouped in pairs, the result will be the same. For example, let's consider addition, which is an associative operation. If you have three numbers, say 2, 3, and 4, you can add them in any order and you'll get the same result:

math
	(2 + 3) + 4 = 9
	 2 + (3 + 4) = 9

In both cases, the result is the same. This property is what makes addition associative. In the context of functional programming, a function f would be associative if for all inputs x,y, and z, the following holds true:

associative
	f(f(x, y), z) = f(x, f(y, z))

In our example:

	compose(toUpper,firstLetter,exclaim) = 
		compose(compose(toUpper,firstLetter),exclaim) =
		 compose(toUpper,compose(firstLetter,exclaim))

Functors ๐Ÿš‚

In the simplest terms, a functor is a type that implements a map operation. In functional programming, it's a way to apply a function over or around some structure that we don't want to alter. The structure could be a list, a tree, or any other data structure - the important part is that the structure is not changed. Let's start with a normal function and then transform it into a functor.

functor-example.js
// consider that large number is any number >= 20
const theFirstLargeNumber = xs => {
	const largeNumbers = xs.filter(x => x>=20);
	return largeNumbers[0];
}
console.log(theFirstLargeNumber[2,3,19,34,140,5]);//34

No, let's transform this solution into a functor way using the Box functor:

functor-example.js
	const Box = (x) => ({
		map: f => Box(f(x)),
		fold: f => f(x)
	})

As we said functor implements a map function.

No, it's time to transform our function into a function way using Box functor.

functor-example.js
	const Box = (x) => ({
		map: (f) => Box(f(x)),
		fold: (f) => f(x),
	});
 
	const theFirstLargeNumber = (xs) =>
		Box(xs)
		 .map(arr => arr.filter(x => x >= 20))
		 .fold(filtered => filtered[0]);
	
	console.log(theFirstLargeNumber([2,3,19,34,140,5])); // 34

Benefits of using functors

Monads ๐Ÿง™โ€โ™‚๏ธ

A monad is a type of functor that also implements a chain (also known as flatMap or bind) method. The chain method is used to sequence operations that return functors. In other words, a monad is a type of data type that wraps a value and provides two methods: map and chain. The map method is used to apply a function to the wrapped value and return a new monad. The chain method is used to "flatten" a nested monad.

Here's a simple example of a monad in JavaScript:

monad.js
const Box = (x) => ({
	map: (f) => Box(f(x)),
	chain: (f) => f(x),
	fold: (f) => f(x),
});
 
const box = Box(2);
const newBox = box.chain(x => Box(x * 2)); // Box(4)
 

You see that The chain and fold methods in a monad do have similar structures, in that they both apply a function to the value inside the monad. However, the key difference lies in what they return and their purpose in the context of working with monads.

The chain method is particularly useful when you have a sequence of operations that each return a monad. For example, consider the following function that parses a string to a number and then increments it:

parse-and-increment.js
 
const Box = (x) => ({
	map: (f) => Box(f(x)),
	chain: (f) => f(x),
	fold: (f) => f(x),
});
 
const parseAndIncrement = (str) =>
	Box(str)
	 .map(s => parseInt(s))
	 .chain(n => Box(n + 1))
	 .fold(x => x);
console.log(parseAndIncrement("4")); // 5

In this example, map(s => parseInt(s)) transforms the string to a number and chain(n => Box(n + 1)) increments the number. Because the increment operation is wrapped in a Box, we use chain instead of map to avoid ending up with a Box inside a Box.

๐Ÿ› ๏ธ Either Monad - Exapmle 1

Let's consider this function, that reads a package.json file and returns the dependencies from it.

getDependecies.js
	const getDependecies = () => {
		try {
			const str = fs.readFileSync("package.json");
			const config = JSON.parse(str);
			return config.dependencies;
		} catch (error) {
		return { 'error': error };
	}
};
 

First, create a Left and Right monads, sometimes referred to as the Either monad because a function can return either a Right value(representing success) or a Left value (representing failure).

either.js
const Right = (x) => ({
	chain: (f) => f(x),
	map: (f) => Right(f(x)),
	fold: (f, g) => g(x),
});
 
const Left = (x) => ({
	chain: (f) => Left(x),
	map: (f) => Left(x),	
	fold: (f, g) => f(x),
});
 

Let's break down it:

Right and Left Monads: The Right and Left functions create monads. The Right monad is used when a computation is successful, and the Left monad is used when there's an error. Both monads have chain, map, and fold methods, but they behave differently. For Right, map and chain apply the function to the value inside the monad. For Left, map and chain ignore the function and return the Left monad as is. The fold method for Right applies the second function (representing the success case), and for Left, it applies the first function (representing the error case).

No, we can create a utility tryCatch function to handle errors in a functionnal way.It will encapsulate operations that might throw exceptions and convert those exceptions into a manageable form.

either.js
	const Right = (x) => ({
		chain: (f) => f(x),
		map: (f) => Right(f(x)),
		fold: (f, g) => g(x),
	});
 
	const Left = (x) => ({
		chain: (f) => Left(x),
		map: (f) => Left(x),
		fold: (f, g) => f(x),
	});
 
	const tryCatch = (f) => {
	  try {
		return Right(f());
	  } catch (e) {
		return Left(e);
	  }
	};

Out tryCatch function provides a way to handle exceptions in a cleaner and more functional way. It takes a function f as an argument and tries to execute it. If f executes successfully, tryCatch returns a Right monad containing the result. If f throws an error, tryCatch catches the error and returns a Left monad containing the error.

Now, create a readFileSync ๐Ÿ“‚ function that will be a wrapper around the fs.readFileSync function. It uses tryCatch to read files, returning a Right monad on success or a Left monad on error, enabling consistent error handling.

either.js
const fs = require("fs");
 
const Right = (x) => ({
  chain: (f) => f(x),
  map: (f) => Right(f(x)),
  fold: (f, g) => g(x),
});
 
const Left = (x) => ({
  chain: (f) => Left(x),
  map: (f) => Left(x),
  fold: (f, g) => f(x),
});
 
const tryCatch = (f) => {
  try {
    return Right(f());
  } catch (e) {
    return Left(e);
  }
};
const readFileSync = (path) => 
		tryCatch(() => fs.readFileSync(path));
 

It's time to rewrite our core function:

either.js
const fs = require('fs');
 
const Right = (x) => ({
  chain: (f) => f(x),
  map: (f) => Right(f(x)),
  fold: (f, g) => g(x),
});
 
const Left = (x) => ({
  chain: (f) => Left(x),
  map: (f) => Left(x),
  fold: (f, g) => f(x),
});
 
const tryCatch = (f) => {
  try {
    return Right(f());
  } catch (e) {
    return Left(e);
  }
};
const readFileSync = (path) => tryCatch(() => fs.readFileSync(path));
 
const getDependencies = () =>
		 readFileSync("package.json")
		.chain((contents) => JSON.parse(contents))
		.map((config) => config.dependencies)
		.fold(
			(error) => ({ error: error }),
			(dependencies) => dependencies
);
const result = getDependencies();

How can we improve itโ“

As you see, we're using JSON.parse and it can throw an error if contents is not valid JSON, and it doesn't return a monad. This could lead to unhandled exceptions and inconsistent behavior. We can improve it by using tryCatch function to handle potential parsing errors and ensure that we always return a monad.

Final Version ๐Ÿชญ

either.js
const fs = require('fs');
 
const Right = (x) => ({
  chain: (f) => f(x),
  map: (f) => Right(f(x)),
  fold: (f, g) => g(x),
});
 
const Left = (x) => ({
  chain: (f) => Left(x),
  map: (f) => Left(x),
  fold: (f, g) => f(x),
});
 
const tryCatch = (f) => {
  try {
    return Right(f());
  } catch (e) {
    return Left(e);
  }
};
const readFileSync = (path) => tryCatch(() => fs.readFileSync(path));
 
const parseJSON = (contents) => tryCatch(() => JSON.parse(contents));
 
const getDependencies = () =>
  readFileSync('package.json')
    .chain((contents) => parseJSON(contents))
    .map((config) => config.dependencies)
    .fold(
      () => 'No dependencies found',
      (dependencies) => dependencies
    );
const result = getDependencies();
 

๐Ÿ“ญ๐Ÿšช Maybe Monad - Exapmle 2

I created a monad called Maybe that represents a value that may or may not be present. (null || undefined)

maybe.js
	const isNothing = (value) => value === null || value === undefined;
 
	const Maybe = (value) => ({
		map: (fn) => isNothing(value) ? Maybe.nothing() : Maybe.just(fn(value)),
		chain: (fn) => isNothing(value) ? Maybe.nothing() : fn(value),
		fold: (f, g) => isNothing(value) ? f() : g(value),
	});
 
	Maybe.just = (value) => Maybe(value);
	Maybe.nothing = () => Maybe(null);
 

map and chain methods are similar as previous ones. The fold method in the Maybe monad is used to extract the value from the monad. It takes two functions as arguments: f and g.

Letโ€™s use this monad now. Letโ€™s write a safeDivide function, which will accept dividend and divisor and return dividend/divisor. We will wrap it in the monad to avoid the side effect. In this case, the only side effect would be division by 0.

maybe.js
const isNothing = (value) => value === null || value === undefined;
 
  const Maybe = (value) => ({
  	map: (fn) => isNothing(value) ? Maybe.nothing() : Maybe.just(fn(value)),
 	chain: (fn) => isNothing(value) ? Maybe.nothing() : fn(value),
 	fold: (f, g) => isNothing(value) ? f() : g(value),
  });
 
Maybe.just = (value) => Maybe(value);
Maybe.nothing = () => Maybe(null);
 
function safeDivide(dividend, divisor) {
  if (divisor === 0) {
   	return Maybe.nothing();
  } else { 
  	return Maybe.just(dividend / divisor);
  }
}
 

Let us explain. If the divisor is 0, no division is possible and we return a new monad with no value, otherwise, we return a monad Maybe(dividend/divisor) with a new value.

Letโ€™s look at a concrete example, and it will lead to a chain of several operations.

maybe.js
  const isNothing = (value) => value === null || value === undefined;
 
  const Maybe = (value) => ({
    map: (fn) => isNothing(value) ? Maybe.nothing() : Maybe.just(fn(value)),
    chain: (fn) => isNothing(value) ? Maybe.nothing() : fn(value),
    fold: (f, g) => isNothing(value) ? f() : g(value),
  });
 
  Maybe.just = (value) => Maybe(value);
  Maybe.nothing = () => Maybe(null);
 
  function safeDivide(dividend, divisor) {
	if (divisor === 0) {
		return Maybe.nothing();
	} else {
		return Maybe.just(dividend / divisor);
	}
  }
 
  const result = Maybe.just(2)
	.chain(x => safeDivide(10, x))
	.chain(x => safeDivide(100, x))
	.fold(
		() => `Error: division by zero`, 
		(res) =>res
	);
  console.log(result); // Output: 20
 

Example 3: List of users from the database

Imagine you want to retrieve a list of users from the database. This function can be disabled if an error occurred during a connection to the database or if the query fails. Come on, letโ€™s put it in Monad and handle the side effects that way.

database.js
 
// Assume this function could fail due to a network error or other issue
const getUsers = () => {
	const users = [
		{ name: 'Alice', email:'alice@example.com' },
		{ name: 'Bob', email: 'bob@example.com' },
		{ name: 'Charlie', email: 'charlie@example.com' }
	];
	return Maybe.just(users);
}
 
// Retrieve the users and extract their names
const names = getUsers()
	.map(users => users.map(user => user.name))
	.map(names => names.map(name => name.toUpperCase()))
	.fold(() => [], (res) => res);
 
console.log(names);
// Output: ['ALICE', 'BOB', 'CHARLIE']

Thanks for your attention! ๐Ÿ™Œ๐Ÿ˜Š