Skip to content
Educora
Advanced22 min19 / 27

Functional programming

Write predictable code: pure functions, immutability, higher-order functions, currying, composition and powerful `reduce` patterns.

Check yourself
In this lesson you will learn
  • Tell pure functions from impure ones and avoid side effects and mutation
  • Update arrays and objects immutably: spread, map, toSorted, structuredClone
  • Build small reusable functions with higher-order functions, currying and composition (pipe/compose)
  • Use reduce to group, count and index data

A bug report arrives: “The basket total is sometimes wrong.” After an hour of searching you find a function that silently changed the array passed to it. Functional programming (FP) is a style that makes such bugs rare: functions only compute and return results, and data never changes behind your back. JavaScript is not a purely functional language, but map, filter, reduce, React and Redux are all built on these ideas.

Pure functions and side effects

Definition
Pure function

1) Always returns the same result for the same arguments; 2) has no side effects: it doesn't change outside variables or its arguments, doesn't print, and doesn't write to files or the network.

No program is free of side effects — at some point you must show or save the result. FP pushes them to the edges of the program: the core consists of pure functions that are easy to test, and input/output happens in a few clear places.

JavaScript
let taxRate = 0.18;
const priceWithTaxImpure = (price) => price + price * taxRate;
const priceWithTax = (price, rate) => price + price * rate;

function addItemImpure(cart, item) {
  cart.push(item);
  return cart;
}
const addItem = (cart, item) => [...cart, item];

const cart = ['tea'];
const newCart = addItem(cart, 'bread');
console.log(cart, newCart);

addItemImpure(cart, 'jam');
console.log(cart);

console.log(priceWithTax(100, 0.18), priceWithTaxImpure(100));
taxRate = 0.2;
console.log(priceWithTax(100, 0.18), priceWithTaxImpure(100));
▸ Expected output
[ 'tea' ] [ 'tea', 'bread' ]
[ 'tea', 'jam' ]
118 118
118 120
The impure function's result depends on the outside taxRate and changed for the same argument. addItemImpure damaged the original cart.

Immutability

Immutability means that instead of changing data you create a new version with the change. Spread is used for arrays and objects, but it copies only one level. In a nested object, copy each level you change; for a full deep copy there is structuredClone. Object.freeze(obj) protects an object from changes, but it also works only on the first level.

JavaScript
const user = { name: 'Aysel', address: { city: 'Baku' }, tags: ['student'] };

const moved = { ...user, address: { ...user.address, city: 'Ganja' } };
const tagged = { ...user, tags: [...user.tags, 'volunteer'] };

console.log(user.address.city, moved.address.city);
console.log(user.tags, tagged.tags);

const shallow = { ...user };
shallow.address.city = 'Shaki';
console.log(user.address.city);

const deep = structuredClone(user);
deep.address.city = 'Lankaran';
console.log(user.address.city, deep.address.city);

const scores = [70, 95, 82];
console.log(scores.toSorted((a, b) => b - a), scores);
▸ Expected output
Baku Ganja
[ 'student' ] [ 'student', 'volunteer' ]
Shaki
Shaki Lankaran
[ 95, 82, 70 ] [ 70, 95, 82 ]
shallow copied only the outer object while address stayed shared — so the original became “Shaki” too. structuredClone copies every level.

Higher-order functions, currying and composition

A higher-order function takes a function as an argument or returns one: map, filter, setTimeout are all like this. Currying turns f(a, b) into f(a)(b): you can supply arguments piece by piece and make ready-made helpers. Composition joins small functions into a conveyor belt: pipe(f, g, h)(x) is the same as h(g(f(x))).

JavaScript
const add = (a) => (b) => a + b;
const multiply = (a) => (b) => a * b;

const addVat = multiply(1.18);
const addDelivery = add(5);
const round2 = (x) => Math.round(x * 100) / 100;

const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x);
const compose = (...fns) => (x) => fns.reduceRight((value, fn) => fn(value), x);

const finalPrice = pipe(addVat, addDelivery, round2);

console.log(finalPrice(10));
console.log([10, 25, 99.99].map(finalPrice));
console.log(compose(round2, addDelivery, addVat)(10));
▸ Expected output
16.8
[ 16.8, 34.5, 122.99 ]
16.8
pipe applies the functions left to right, compose right to left, as in maths. round2 hides floating-point noise: 10 * 1.18 actually gives 11.799999999999999.

reduce patterns

reduce can build any structure from an array: a sum, counters, groups, an index by id, a Map. Always give an initial value — on an empty array, reduce without one throws an error. For grouping, modern browsers also have the ready-made Object.groupBy (ES2024).

JavaScript
const orders = [
  { id: 1, customer: 'Murad', city: 'Baku', total: 40 },
  { id: 2, customer: 'Leyla', city: 'Ganja', total: 25 },
  { id: 3, customer: 'Murad', city: 'Baku', total: 15 },
  { id: 4, customer: 'Elvin', city: 'Baku', total: 60 },
];

const revenue = orders.reduce((sum, order) => sum + order.total, 0);

const countByCity = orders.reduce((acc, { city }) => {
  acc[city] = (acc[city] ?? 0) + 1;
  return acc;
}, {});

const byId = orders.reduce((acc, order) => ({ ...acc, [order.id]: order.customer }), {});

const totalByCustomer = orders.reduce(
  (map, { customer, total }) => map.set(customer, (map.get(customer) ?? 0) + total),
  new Map(),
);

console.log(revenue);
console.log(countByCity);
console.log(byId);
console.log(totalByCustomer);
▸ Expected output
140
{ Baku: 3, Ganja: 1 }
{ '1': 'Murad', '2': 'Leyla', '3': 'Murad', '4': 'Elvin' }
Map(3) { 'Murad' => 55, 'Leyla' => 25, 'Elvin' => 60 }
Exercise

Write a pure function applyDiscount(products, percent): it returns a new array with discounted prices, and products stays unchanged. New price: price * (100 - percent) / 100.

Exercise · JavaScript
const products = [
  { name: 'Pen', price: 2 },
  { name: 'Notebook', price: 5 },
];

function applyDiscount(products, percent) {
  // return a new array; do not change products
}

console.log(applyDiscount(products, 20));
console.log(products);
▸ Expected output
[ { name: 'Pen', price: 1.6 }, { name: 'Notebook', price: 4 } ]
[ { name: 'Pen', price: 2 }, { name: 'Notebook', price: 5 } ]
Exercise

Build slugify from small functions with pipe: trim the text, make it lowercase, split it into words (dropping empty pieces) and join the words with -.

Exercise · JavaScript
const pipe = (...fns) => (x) => fns.reduce((value, fn) => fn(value), x);

const trim = (text) => text.trim();
const lower = (text) => text.toLowerCase();
// write words(text) and joinWithDash(words)

const slugify = pipe(trim, lower);

console.log(slugify('  Learn JavaScript Today '));
console.log(slugify('Functional   Programming'));
▸ Expected output
learn-javascript-today
functional-programming

Key points

  • A pure function depends only on its arguments and has no side effects — it is easy to test and reuse.
  • Don't mutate data you receive: create new arrays and objects with spread, map, filter, toSorted.
  • Spread makes a shallow copy; copy every nested level you change or use structuredClone.
  • Higher-order functions take or return functions; currying pre-fills arguments, and pipe/compose chain small functions.
  • reduce with an initial value can sum, count, group and index data.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Which function is pure?