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.
- Pure Function
- Deterministic Function
- Higher-order Function
- Immutability
- Currying or Partial Function
- 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.
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.
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.
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))
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:
- Functional data structures
- Handling errors without exceptions
- Strictness and laziness(Lazy evaluation)
- Functional parallelism(async FP)
- Monoids and Functor
- 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: