DEV Community

Kurapati Mahesh
Kurapati Mahesh

Posted on

Unlocking the Power of Functional Programming in JavaScript: A Comprehensive Guide

Introduction:

A functional programming(FP) is paradigm, means a way of thinking about software construction based on some principles like Pure functions, Immutability, First class and higher-order functions, Function composition, Closure, Declarative Programming, Recursion, Referential Transparency, Currying and Partial Application

These principles, when applied effectively in JavaScript, can lead to code that is more modular, maintainable, resilient, more understandable, testable, and capable of elegantly handling complex problems.

This article looks quite long but not theoretical as such.

Let's start experimenting each item:

1. Pure Functions:

Two Rules

  1. Given the same input, always return same output.
  2. Produces no side effects

Use: Easy to refactor, makes code more flexible and adaptable.

Example 1:

// Impure function.
let a = 4;
const multiplyNumbers = (b) => a *= b;

multiplyNumbers(3);
console.log(a); // first time: 12
> 12
multiplyNumbers(3);
console.log(a); // second time: 36
> 36

// Mutates external variable so it isn't pure.
Enter fullscreen mode Exit fullscreen mode
// Pure function.
const multiplyNumbers = (x,y) => x * y;

multiplyNumbers(2, 3);
> 6
Enter fullscreen mode Exit fullscreen mode

Example 2:

// Impure function.
addNumberarr = (arr, num) => {
arr.push(num);
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);

console.log(testArr);
> [1, 2, 3, 4]

// Mutates input array so it isn't pure.
Enter fullscreen mode Exit fullscreen mode
// pure version of above.
addNumberarr = (arr, num) => {
return [...arr, num];
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);
> [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

JS Built-in Pure functions:

arr.reduce()
arr.map()
arr.filter()
arr.concat()
arr.slice()
arr.each()
arr.every()
... - spread syntax
Enter fullscreen mode Exit fullscreen mode

JS Built-in Impure functions:

arr.splice()
arr.push()
arr.sort()
Math.random()
Enter fullscreen mode Exit fullscreen mode

2. Immutability:

Objects whose state can't be altered once it is created.

A simple example would be using the slice method to help you easily grasp the meaning.

const arr = [1,2,3,4];

const slicedArray = arr.slice(1,2);

slicedArray
> [2]

arr
> [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

If you see above example, slice didn't alter or modify or mutate original array arr. Whereas, if you see below example:

const arr = [1,2,3,4];

arr.push(5);
> 5

arr
> [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

original array arr has been mutated. It's not that we shouldn't use push but we can avoid in most of the situations. Simple example would be:

const arr = [1,2,3,4];

const newArr = [...arr, 5];

arr
> [1, 2, 3, 4]

newArr
> [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

The above all are simple examples and won't create any issues possibly. But, situations where we keep on modifying same object wherever possible across whole file will create many issues. As we need to maintain the track as how many times and ways that object has been altered.

So, to solve this issue we need to avoid mutating the object.

3. First Class functions

First-class functions refer to the concept of treating functions as first-class citizens, which means they are treated as regular variables or values. It enable functions to be manipulated and used in the same ways as other data types like strings or numbers. This allows functions to be passed as arguments to other functions, returned as values from other functions, and assigned to variables. Javascript supports this.

It opens the door to powerful programming techniques, such as higher-order functions, function composition, and the creation of abstractions.

4. Higher-Order Functions:

A function can take a function as an argument or can return a function as a value is called a higher-order function.

  1. A function that returns a function
const higherOrderFunc = function() {
    return function() {
        return 12;
    }
}

// which returns below function hence it is higher order function.
higherOrderFunc(); 
> ƒ () {
        return 12;
    }

higherOrderFunc()();
> 12
Enter fullscreen mode Exit fullscreen mode
  1. A function that takes a function as an argument
const testFunc = function(x) {
    return x + 12;
}

//which takes function as an argument.
const higherOrderFunc = function(testFunc) {
    return testFunc(8);
}

higherOrderFunc(testFunc);
> 20
Enter fullscreen mode Exit fullscreen mode

Example 1:

function calculate(operation, numbers) {
    return operation(numbers);
}

function addition(numbers) {
    let sum = 0;
    for (const number of numbers) {
        sum+=number;
    }
    return sum;
}

function multiply(numbers) {
    let sum = 1;
    for (const number of numbers) {
        sum*=number;
    }
    return sum;
}

const numbers = [1,2,3,4,5];
console.log(calculate(addition, numbers));
> 15

console.log(calculate(multiply, numbers));
> 120
Enter fullscreen mode Exit fullscreen mode

calculate(multiply, numbers) - Don't append parenthesis while sending function as an argument.

Benefits of higher-order functions:

Reduces code duplication
Single responsibility

In Javascript, functions can take arguments as primitives or objects and return the same called first-order functions.

JS built-in higher order functions are:

arr.reduce(), arr.forEach(), arr.filter(), arr.map()

5. Functional Composition:

It is an approach where result of one function passed on to next function.

const add = (x, y) => x+y;

const subtract = (x) => x-4;

const multiply = (x) => x * 8;

// result of `add` is passed to `subtract` and its result passed to `multiply`.
const result = multiply(subtract(add(2, 3)));

result;
> 8
Enter fullscreen mode Exit fullscreen mode

That looks readable but what if we have more functions to call one after other. Let's try little cleaner approach.

const compose = (...functions) => x => functions.reduceRight((total, f) => f(total), x);

const add = x => x+2;

const subtract = x => x-1;

const multiply = x => x * 8;

compose(multiply, subtract, add)(2);
> 24
Enter fullscreen mode Exit fullscreen mode

We can also use reduce to implement:

const pipe = (...functions) => x => functions.reduce((total, f) => f(total), x);

const add = x => x+2;

const subtract = x => x-1;

const multiply = x => x * 8;

pipe(add, subtract, multiply)(2);
> 24
Enter fullscreen mode Exit fullscreen mode

pipe - performs from left-to-right.
compose - performs from right-to-left.

6. Declarative Programming:

Declarative: tells What to do

Imperative: tells How to do

Example: Find the employees with dept 'justCode' and summation of their salary.

Imperative Style:

const employees = [
{id: 1, name: 'james', dept: 'admin', salary: 10000},
{id: 1, name: 'Tom', dept: 'finance', salary: 10000},
{id: 1, name: 'peter', dept: 'justCode', salary: 12500},
{id: 1, name: 'tunner', dept: 'justCode', salary: 14500},
];

const justCodeDept = [];

// filter employees based on dept name.
for (let i=0; i<employees.length; i++) {
  if (employees[i].dept === 'justCode') {
    justCodeDept.push(employees[i]);
  }
}

// summation of justCodeDept employees.
let summation = 0;
for (j = 0; j<justCodeDept.length; j++) {
  summation = summation + justCodeDept[j].salary;
}

console.log(summation);
Enter fullscreen mode Exit fullscreen mode

Declarative Style:

const employees = [
{id: 1, name: 'james', dept: 'admin', salary: 10000},
{id: 1, name: 'Tom', dept: 'finance', salary: 10000},
{id: 1, name: 'peter', dept: 'justCode', salary: 12500},
{id: 1, name: 'tunner', dept: 'justCode', salary: 14500},
];

console.log(employees.filter(item => item.dept === 'justCode').reduce(((previousValue, currentValue) => previousValue += currentValue.salary), 0));
Enter fullscreen mode Exit fullscreen mode

7. Currying:
Splitting up function that takes multiple arguments into a sequence of functions that each take its individual argument (only one).

Example 1:

Generally, we write:


function addition(x, y, z) {
    return x + y + z;
}

addition(1, 2, 3);
> 6
Enter fullscreen mode Exit fullscreen mode

Currying:

function addition(x) {
    return function addY(y) {
        return function addz(z) {
            return x+y+z;
        }
    }
}

addition(1)(2)(3);
> 6
Enter fullscreen mode Exit fullscreen mode

Using arrow functions:

addition = (x)=>(y)=>(z)=>x + y + z;

addition(1)(2)(3);
> 6
Enter fullscreen mode Exit fullscreen mode

Example 2:

function formWelcomNote(name) {
    name = `Hello ${name}, `;
    return function(location) {
        location = `Welcome to ${location},`;
        return function(section){
            return `${name}${location} Please visit ${section} section`
        }
    }
}

formWelcomNote('Yester')('VK Just Code Articles')('JS Articles');
> 'Hello Yester, Welcome to VK Just Code Articles, Please visit JS Articles section'
Enter fullscreen mode Exit fullscreen mode

We can also write as:

formWelcomNote = (name)=>{
    name = `Hello ${name}, `;
    return (location)=>{
        location = `Welcome to ${location},`;
        return (section)=>{
            return `${name}${location} Please visit ${section} section`
        }
    }
}

formWelcomNote('Yester')('VK Just Code Articles')('JS Articles');
> 'Hello Yester, Welcome to VK Just Code Articles, Please visit JS Articles section'
Enter fullscreen mode Exit fullscreen mode

Example 3:

function calculation(fn) {
    switch (fn) {
        case 'add': return (a, b) => a + b;
        case 'sub': return (a, b) => a - b;
        case 'mul': return (a, b) => a * b;
        case 'div': return (a, b) => a / b;
    }
}
console.log(calculation('mul')(4, 2));
Enter fullscreen mode Exit fullscreen mode

8. Partial Application:

You fix a certain number of arguments for a function and generate a new function with fewer parameters. This new function can be called with the remaining arguments at a later time. Partial application helps in creating more specialized and reusable functions.

Example:

function add(a, b) {
  return a + b;
}

// Partially apply the first argument
const add2 = multiply.bind(null, 2);

console.log(add2(5));  // Output: 7 (2 + 5)
console.log(add2(8));  // Output: 10 (2 + 8)
Enter fullscreen mode Exit fullscreen mode

9. Referential Transparency:

An expression in javascript can be replaced by its value is called referential transparency.

const add = (x,y)=>x + y;

const multiply = (x)=>x * 4;

// add (3, 4) can be replaced by 7. - Referential Transparency.

multiply(add(3, 4)); 
> 28

multiply(add(3, 4));
> 28
Enter fullscreen mode Exit fullscreen mode
const arr = [];
const add = (x,y)=>{
    const addition = x + y;
    arr.push(addition);
    return addition;
}

const multiply = (x)=>x * 4;

// Here, we can't replace add(3,4) with 7 as it affects the program
multiply(add(3, 4));
> 28

> multiply(add(3, 4));
28
Enter fullscreen mode Exit fullscreen mode

10. Closure:

Closure gives you access to an outer functions scope from inner function.

function outer() {
    const name = 'test';
    function inner() {
     // 'name' from outer function is accessible inside inner function
     console.log(name);
    }
    inner();
}
outer();

> test
Enter fullscreen mode Exit fullscreen mode
function outerAdd(x) {
    return function(y) {
        return x + y;
    };
}

const outer12 = outerAdd(12); // x as 12.
const outer14 = outerAdd(14); // x as 14.

const outer12Result = outer12(12); // y as 12.
console.log(outer12Result);
> 24

const outer14Result = outer14(14); // y as 14.
console.log(outer14Result);
> 28
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can use arrow functions as well like below.

outerAdd = x => y => x + y;

const outer12 = outerAdd(12);
const outer14 = outerAdd(14);

const outer12Result = outer12(12);
console.log(outer12Result);
> 24

const outer14Result = outer14(14);
console.log(outer14Result);
> 28
Enter fullscreen mode Exit fullscreen mode

Counter example using closure:

function outer() {
    let counter = 0;
    return function inner() {
        counter += 1;
        return counter;
    }
}
const out = outer();

console.log(out());
console.log(out());
console.log(out());

> 1
> 2
> 3
Enter fullscreen mode Exit fullscreen mode

11. Recursion:

Recursion is a programming technique in which a function calls itself to solve a problem.

Example:

function factorial(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return n * factorial(n - 1);
  }
}

console.log(factorial(5));  // Output: 120 (5 * 4 * 3 * 2 * 1)
console.log(factorial(0));  // Output: 1 (by definition)
Enter fullscreen mode Exit fullscreen mode

In this example, the factorial function calculates the factorial of a given number n. It uses the base cases of n === 0 and n === 1, where the factorial is defined to be 1. For any other value of n, the function recursively calls itself with n - 1 and multiplies the result by n.

When you call factorial(5), the sequence of recursive calls looks like this:

factorial(5)
  -> 5 * factorial(4)
       -> 4 * factorial(3)
            -> 3 * factorial(2)
                 -> 2 * factorial(1)
                      -> 1
                 <- 2 * 1 = 2
            <- 3 * 2 = 6
       <- 4 * 6 = 24
  <- 5 * 24 = 120
Enter fullscreen mode Exit fullscreen mode

please do comment any examples on any concept.

Thanks.
urstrulyvishwak

Top comments (4)

Collapse
 
koteisaev profile image
Kote Isaev - #StandWithUkraine • Edited

Recursive functions harder to troubleshoot and debug and in many case, including factorial, it can be rewritten to use single loop instead of recursion.
One of core problems if recursion is max stack size limit, added to crash in case if something went wrong, like unintentional usage of recursion or forgot to add a condition to recursion function call to make sure recursive function will exit the calls chain.

Collapse
 
urstrulyvishwak profile image
Kurapati Mahesh

Yes. need to use very carefully.

Collapse
 
juanfrank77 profile image
Juan F Gonzalez

Functional programming is love, functional programming is life.

Collapse
 
respect17 profile image
Kudzai Murimi

Well Explained hey !