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
- Given the same input, always return same output.
- 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.
// Pure function.
const multiplyNumbers = (x,y) => x * y;
multiplyNumbers(2, 3);
> 6
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.
// pure version of above.
addNumberarr = (arr, num) => {
return [...arr, num];
};
const testArr = [1,2,3];
addNumberarr(testArr, 4);
> [1, 2, 3, 4]
JS Built-in Pure functions:
arr.reduce()
arr.map()
arr.filter()
arr.concat()
arr.slice()
arr.each()
arr.every()
... - spread syntax
JS Built-in Impure functions:
arr.splice()
arr.push()
arr.sort()
Math.random()
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]
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]
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]
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.
- 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
- 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
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
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
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
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
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);
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));
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
Currying:
function addition(x) {
return function addY(y) {
return function addz(z) {
return x+y+z;
}
}
}
addition(1)(2)(3);
> 6
Using arrow functions:
addition = (x)=>(y)=>(z)=>x + y + z;
addition(1)(2)(3);
> 6
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'
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'
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));
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)
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
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
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
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
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
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
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)
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
please do comment any examples on any concept.
Thanks.
urstrulyvishwak
Top comments (4)
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.
Yes. need to use very carefully.
Functional programming is love, functional programming is life.
Well Explained hey !