Functional Programming In JavaScript/TypeScript for Beginners

Functional Programming In JavaScript/TypeScript for Beginners

Functional programming is one of the misleading topics in the programming world. You will find multiple articles and get confused. So how should...

TypeScript or say JavaScript, is not designed to be functional programming(FP). JavaScript is a prototype-based object-oriented language. Meaning, Everything in the language is wrapped in an object. When you create a variable or constant underline it uses an object. So the question, How does JavaScript support functional programming? To answer the above question, There is Sugar Syntax and auto wrapping. These help developer to write functional way without worrying too much. Let's understand in thorough.

Note: All code example has written in TypeScript. You can use TypeScript Playground to convert TS to JS code.

Basic Principles

Before going further, You should know some of the basic principles as described below.

  1. Pure Function
  2. Deterministic Function
  3. Higher-order Function
  4. Immutability
  5. Currying or Partial Function
  6. Function composition

1. Pure Function

Pure functions are deterministic function with no side-effect. Meaning, The out will be always the same for the same inputs. Same time, it will not consume any other global variables.

Pure Function

Example of Pure Function:

function calculateInterest(p: number, t: number, r: number) {
  return (p * t * r) / 100;
}

console.log(calculateInterest(1000, 5, 5));

//250

Here, No matter what for the same inputs, the output will be the same.

The advantage of having a pure function, It is easy to understand and test. However, It is very hard to build an entire Application just using Pure Function. We needed some deterministic function too.

Note: React.js Pure components are deterministic in nature, but they are not a pure function. It has side-effects in it like React.createElement which create document elements.

2. Deterministic Function

A Deterministic function is where the output of the function is always deterministic. It may have side-effect but still, the output should not change with time. The same input should result in the same output

Simple Example:

function trim(str: string) {
  return str.replace(/^\s+|\s+$/g, "");
}

console.log(trim("    This text had extra spaces.. "));

// "This text had extra spaces.."

Another Example:

A deterministic function can have a side-effect. Meaning, It can have access to global variables.

enum GENDER {
  MR = "Mr.",
  MS = "Miss.",
}

function greeting(name: string, is: GENDER = GENDER.MR) {
  return `Hello, ${is == GENDER.MR ? GENDER.MR : GENDER.MS} ${name}`;
}

console.log(greeting("Deepak"));

// "Hello, Mr. Deepak"

Example of a Non-Deterministic Function:

function rand(num = 4) {
  return Math.random().toString(16).substr(-num);
}
function uuid(): string {
  return [rand(8), rand(4), rand(4), rand(4), rand(12)].join("-");
}
console.log(uuid());

In the above example, the method rand using Math.random to generate a random number. The output of this API will be non-deterministic.

The good thing about a deterministic function that it is very common in any language. It is easy to create and understand. However, having side-effect in it. Some time is hard to test.

3. HOF- Higher-Order Function

A HOF can take function|s as input and can return a function as output.

Example:

const groupBy = <T = any>(fn: (item: T, i: number) => any, data: T[]) =>
  data.map(fn).reduce((acc, val, i) => {
    acc[val] = (acc[val] || []).concat(data[i]);
    return acc;
  }, {});

const { odds, evens } = groupBy((item) => (item % 2 == 0 ? "evens" : "odds"), [
  1,
  2,
  3,
  4,
  5,
]);
console.log(odds, evens);

// [ 1, 3, 5 ] [ 2, 4 ]

Note: Here in the above example, groupBy is a function. You may have noticed that I have not used the keyword function to create a function. Since I am using lambda as a function and assign it to a variable. After ES6/ES2015, JavaScript introduced the concept of lambda along with many more new syntax. Mostly lot of them are sugar syntax around the real implementation. You can read more here.

groupBy is a function that takes an input fn as a function and data as an array. After computation on all item in the array, it returns an object of key and values.

Note: Just like HOF, HOC in React is a component that can take another component|s as input and can return another component.

4. Immutability

Immutability is a concept where once data/variable is created can't be changed over a period of time. The idea to avoid the data race in cross-sharing environments like async programming, side-effects.

immutable javascript

JavaScript does have some Immutability APIs. However, Those are not enough. Let's see some of the examples.

const PI = 3.141592653589793;

// PI = 1
// Cannot assign to 'PI' because it is a constant.

const numbers = Object.freeze([1, 2, 3, 4]);

// Just like normal array, you can map on values
numbers.forEach((x) => console.log(x));

// numbers.push()

// Cannot add property 4, object is not extensible
// OR in TS, Property 'push' does not exist on type 'readonly number[]'

Since JavaScript is a dynamic language. Meaning, data can be changed on runtime. This makes it tough to implement Immutability in JavaScript. Same time, not all member/object support immutability. You can use immutable-js. However, I recommend making immutability a practice and not including another library.

Simple way to achieve Immutability

Array:

const numbers = Object.freeze([1, 2, 3, 4]);

// Just like normal array, you can loop on values
numbers.forEach((x) => console.log(x));

// Copy array and add new

const anotherNumbers = [...numbers].concat(5);

//[1,2,3,4,5]

Object:

const configs = {
  API_URL: "some random urls",
  TIMEOUT: 30 * 60 * 10000, //in ms
};

const newConfigs = {
  ...configs,
  SERVER_TIMEOUT: 100 * 60 * 10000, //in ms
};
// OR

const newConfigs2 = Object.assign({}, newConfigs, {
  SERVER_TIMEOUT: 100 * 60 * 10000,
});

Map:

const details = new Map([
  ["name", "deepak"],
  ["address", "some where in world"],
]);

for (let key of details.keys()) {
  console.log(key);
}
//"name" "address"

const updatedDetails = new Map([
  ...details.entries(),
  ["newAddress", "still some where in world"],
]);
for (let key of updatedDetails.keys()) {
  console.log(key);
}
//"name" "address" "newAddress"

5. Currying or Partial Function

Currying is a method or technique in FP, Where a function can be composed to take input partially. Meaning, If a function sum take input a and b as arguments. Currying that function can make sum function take one argument a and return another function. We can use that newly created function to do summation later.

Currying or Partial Function

Let's see from example:

const split = (token = /\s+/, str = "") => str.split(token);

const splitByHash = (str: string) => split(/#/, str);

console.log(splitByHash("This#is#awesome"));

// [ 'This', 'is', 'awesome' ]

In the above example, the split function takes token and string data to split in. We have created a function splitByHash where the token is already defined. It just takes a string to split it. Here, splitByHash is a partial function.

Note: Above example is a good demonstration for the partial function. However, creating a partial function like this is not scalable for more than 2/3 arguments. We can use some basic utility to create a partial function or curried function.

// Helper method, Curries a function.
// https://decipher.dev/30-seconds-of-typescript/docs/curry

const curry = (fn: Function, arity = fn.length, ...args: any[]): any =>
  arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);

const split = (token = /\s+/, str = "") => str.split(token);

const splitByHash = curry(split, 2)("#");

console.log(splitByHash("This#is#awesome"));
// [ 'This', 'is', 'awesome' ]

6. Function composition

Function composition is a mathematical concept where an operation takes two functions f and g and produces a function h such that h(x) = g(f(x)). For simplicity, (g º f)(x) = g(f(x))

function-composition

Let's see from example:

// Helper method, Performs right-to-left function composition.
// https://decipher.dev/30-seconds-of-typescript/docs/compose/

const compose = (...fns: Function[]) =>
  fns.reduce((f, g) => (...args: any[]) => f(...[g(...args)]));

const add10 = (x: number) => x + 10;
const multiply = (x: number, y: number) => x * y;
const multiplyAndAdd5 = compose(add10, multiply);
console.log(multiplyAndAdd5(5, 2)); // 20

In this example, you can see, multiplyAndAdd5 is composed right to left. It's first multiply 2 number(5x2 = 10). And takes it out and add 10.

Let see another example: Get average age of active users

const sum = (nums: number[]) => nums.reduce((s, i) => s + i, 0)
const average = (nums: number[]) => sum(nums) / nums.length


const getActiveUserAges = (data: UserType[] = []) => data.filter(user => user.active === true).map(u => u.age)


const users = [
  {
    name: "deepak",
    age: 31,
    active: true
  },
  {
    name: "sandy",
    age: 20,
    active: false
  },
  {
    name: "unknown",
    age: 35,
    active: true
  }
]
type UserType = typeof users[0]

const activeUsers = getActiveUserAges(users)

const  = sum(activeUsers)


console.log(sumOfAges/ activeUsers.length)

In the above example to get the average age of the active user, We have to call multiple methods. This is good. But we can make this more declarative using compose. If we have to write something functionally. It will look like as below

// helper method

const compose = (...fns: Function[]) =>
  fns.reduce((f, g) => (...args: any[]) => f(...[g(...args)]));

// sum of ages
const getSumAges = compose(sum, getActiveUserAges);
console.log(getSumAges(users));

// average of ages
const getAverageOfAges = compose(average, getActiveUserAges);
console.log(getAverageOfAges(users));

As you can see, adding compose make it easy to mixing function and create another function. That can be reused later.

Some Real-Life Problems

Now since we know the basics of FP. Let's explore with example.

async function main() {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/") // fetch todos
    .then((r) => r.json()); // then extract json
  // Response Todo[]
  // Todo {userId: number, id: number, title: string, completed: boolean}
  const todos = response; // extract Todo values
  console.log(todos[0]); // [{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}, ...]

  const completedTodos = todos.filter((todo) => todo.completed);

  const usersIds = completedTodos.map((todo) => todo.userId);

  const getAllUserById = (id) =>
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((r) =>
      r.json()
    );

  const userWhoCompletedTods = await Promise.all(usersIds.map(getAllUserById));
  console.log(userWhoCompletedTods.length);
  console.log(userWhoCompletedTods[0]);
}
main();

In the above example, First, we are fetching some todos using the rest API. After that, we are filtering todo based on its status completed. Once we have all complete todos, We are collecting ids and to fetch all users.

All good. However, If we have to change one simple behaviour. Fetch users who have not completed todos. Even though it is simply not changing. But will change all coding declaration. We do have to change all variable name. Which is a tedious job to do. We can make this program more declarative using FP. For that we do need some helper methods:

Helper methods: 30-seconds-of-typescript

const curry = (fn, arity = fn.length, ...args) =>
  arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);

const filter = (fn, data) => data.filter(fn);
const map = (fn, data) => data.map(fn);
const not = (fn) => (...args) => !fn(...args);
const prop = curry((key, data) => data[key]);

Now let's rewrite the same program

const isCompleted = (item) => item.completed;
const isNotCompleted = not(isCompleted);
const getId = prop("id");

const userWhoCompletedTodos = map(getId, filter(isCompleted, todos));
const usersWhoNotCompletedIds = map(getId, filter(isNotCompleted, todos));

console.log(userWhoCompletedTodos.length, usersWhoNotCompletedIds.length);

As you can see, Adding only a few helpers makes it so simple to switch between isCompleted to isNotCompleted. And the same time it is very expressive on its own.

Note: There are some other aspects of Functional Programming. I have not covered them all. The reason, Either those are too complex for the scope of this article or not relevant to JavaScript itself.

Some worth mentioning concepts:

  1. Functional data structures
  2. Handling errors without exceptions
  3. Strictness and laziness(Lazy evaluation)
  4. Functional parallelism(async FP)
  5. Monoids and Functor
  6. Side-effect

Where to go from here

As I already mentioned, JavaScript is not a fully functional language. Same time as JavaScript developer, We have to use document and window in frontend. So we completely can not ignore impurity. We have to mix and match. Writing function way makes your code more declarative and readable. However, It does add a little bit of complexity in code in term of core concepts. Saying that, If you know the basic concept as described above. You are good to go and write FP. Same time there are multiple articles online. I have listed some of them below. If you really want to learn FP. I will do recommend to write some code in language like scala, clojure, haskell. It will help you to understand the core concepts and idea behind FP.

Note: Due to content length, I have to break this topic in multiple articles. Soon i will publish next part of it.

Reference articles:

  1. An introduction to functional programming in JavaScript
  2. 9 Functional Programming Concepts Everyone Should Know
  3. Understand the Key Functional Programming Concepts
  4. 30-seconds-of-typescript/
  5. scala-book/functional-programming
  6. An-introduction-to-the-basic-principles-of-functional-programming

Did you find this article valuable?

Support Deepak Vishwakarma by becoming a sponsor. Any amount is appreciated!