Functional Programming
Reviewed & published by Brayan K
Functional programming is a style of writing code that builds programs by composing pure functions, avoiding shared state and mutable data, and treating functions as values you can pass around and combine.
Part of the free JavaScript course at LearnCodingFast β hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Pure Functions & Side Effects
- Immutability principles
- First-class & Higher-Order Functions
- Function Composition & Pipelines
- Currying & Partial Application
- Recursion vs Loops
π‘ Running Code Locally: While this online editor runs real JavaScript, some advanced examples (like fetch to external APIs) may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
Functional Programming (FP) is one of the most powerful programming paradigms in modern JavaScript. It influences React, Redux, Node.js data pipelines, AI inference systems, functional APIs, distributed systems, and even modern JS engines. FP focuses on writing predictable, testable, bug-resistant code by avoiding mutation, using pure functions, and treating functions as first-class values.
Many beginner developers learn FP only at the surface level (e.g., .map, .filter, .reduce), but real mastery requires understanding purity, immutability, referential transparency, function composition, closures, recursion, higher-order functions, and lazy evaluation.
π₯ Core Principle 1: Pure Functions
A pure function always returns the same output for the same input and has zero side effects.
let score = 0;
function addPoints(x) {
score += x; // modifies external state
}
addPoints(5);
console.log(score); // 5function addPoints(score, x) {
return score + x;
}
console.log(addPoints(10, 5)); // 15
console.log(addPoints(10, 5)); // 15 (same result)Pure functions are predictable, testable, and safe β perfect for AI, finance, games, and backend logic.
π₯ Core Principle 2: Immutability
FP avoids changing existing data. Instead, it creates new copies.
const user = { name: "Boopie", age: 16 };
user.age = 20; // mutates original object
console.log(user.age);const user = { name: "Boopie", age: 16 };
const updatedUser = { ...user, age: 20 };
console.log(user.age); // 16 (original unchanged)
console.log(updatedUser.age); // 20This avoids unexpected bugs, especially in UI frameworks like React.
π₯ Core Principle 3: First-Class & Higher-Order Functions
- Functions can be stored in variables
- Passed as arguments
- Returned from other functions
function applyTwice(fn, value) {
return fn(fn(value));
}
const double = x => x * 2;
console.log(applyTwice(double, 5)); // 20This is the foundation of .map, .filter, .reduce, and all declarative patterns.
π₯ Core Principle 4: Function Composition
Composition is the idea of combining small functions to create powerful pipelines.
const double = x => x * 2;
const square = x => x * x;
const compose = (f, g) => x => f(g(x));
const doubleThenSquare = compose(square, double);
console.log(doubleThenSquare(3)); // 36 (3 * 2 = 6, 6 * 6 = 36)This is how frameworks like RxJS and Redux Toolkit work internally.
π₯ Real-World Benefits of FP
β Fewer Bugs
Pure functions + no mutation = predictable code
β Easier Testing
You don't need mocks when everything is pure
β Better Parallelization
Pure functions can run on multiple threads safely
β Reusable Logic
Small functions compose into complex behaviors
β More Declarative
You describe what to do, not how to do it
π₯ Functional vs Imperative Code
Imperative code focuses on how something works:
const arr = [1, 2, 3, 4];
const doubled = [];
for (let i = 0; i < arr.length; i++) {
doubled.push(arr[i] * 2);
}
console.log(doubled);Functional code focuses on what you want:
const arr = [1, 2, 3, 4];
const doubled = arr.map(x => x * 2);
console.log(doubled);Less code β fewer bugs β faster development.
π₯ Higher-Order Functions in Real Depth
A higher-order function (HOF) is a function that either takes another function as an argument or returns a function.
function makeMultiplier(multiplier) {
return function (value) {
return value * multiplier;
};
}
const triple = makeMultiplier(3);
console.log(triple(10)); // 30
console.log(triple(5)); // 15This technique is used by:
- Express middleware
- Redux enhancers
- Functional utilities (Ramda, LodashFP)
- AI prompt engineering libraries
π₯ Currying β Single-Argument Chains
Currying increases flexibility and readability by building functions one argument at a time.
function add(a) {
return function (b) {
return a + b;
};
}
console.log(add(5)(10)); // 15
// Real use case: dynamic filters
const greaterThan = x => y => y > x;
const greaterThan10 = greaterThan(10);
console.log([5, 12, 20].filter(greaterThan10));
// [12, 20]This is how reusable filtering logic is built in professional apps.
π₯ Partial Application
Partial application fixes some arguments and supplies the rest later.
function calculatePrice(taxRate, discount, price) {
return price * taxRate - discount;
}
const ukPrice = calculatePrice.bind(null, 1.2);
const ukPriceWith5Discount = ukPrice.bind(null, 5);
console.log(ukPriceWith5Discount(100)); // 115- E-commerce platforms
- Billing engines
- Gaming score algorithms
- Configuration-based systems
π₯ Recursion β Replacing Loops
Functional programming prefers recursion for repetitive tasks.
function sum(arr) {
if (arr.length === 0) return 0;
return arr[0] + sum(arr.slice(1));
}
console.log(sum([1, 2, 3, 4])); // 10
// Factorial example
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120- DOM rendering
- AI tree search
- AST parsing (compilers)
- React reconciliation
- File system crawlers
π₯ Closures in FP β Hidden Power
Closures allow functions to "remember" variables even after the outer function finishes.
function createCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3- Data privacy
- Encapsulation
- Custom hooks
- State managers
Closures turn functions into stateful machines without using classes.
π₯ Memoization β Pure Function Optimization
Memoization stores the results of expensive function calls so they aren't recalculated.
function memoize(fn) {
// A Map, not a plain object. A Map has no inherited keys, so looking up
// "toString" cannot accidentally hit something on Object.prototype.
const cache = new Map();
return function (arg) {
// Ask "have I seen this key?" β never "is the cached value truthy?".
// A truthiness check would recompute forever for results like 0, "" or false.
if (cache.has(arg)) return cache.get(arg);
const result = fn(arg); // first time only: do the real work
cache.set(arg, result); // remember it for next time
return result;
};
}
let calls = 0;
const slowDouble = n => {
calls++; // count how often the real work runs
for (let i = 0; i < 1e6; i++) {} // stand-in for something expensive
return n * 2;
};
const fastDouble = memoize(slowDouble);
console.log(fastDouble(5)); // 10 β computed, so slowDouble ran
console.log(fastDouble(5)); // 10 β served straight from the cache
console.log(fastDouble(0)); // 0 β a falsy result caches correctly too
console.log(fastDouble(0)); // 0 β from the cache
console.log("slowDouble actually ran", calls, "times");
// β
Expected output:
// 10
// 10
// 0
// 0
// slowDouble actually ran 2 times- Data pipelines
- Caching layers
- AI inference
- Graph computations
- React rendering optimizations
π₯ Declarative Data Flow: Map, Filter, Reduce
Functional programming replaces loops with transformations.
// map β transforms data
const doubled = [1, 2, 3].map(x => x * 2);
console.log(doubled); // [2, 4, 6]
// filter β keeps only items that match a condition
const evens = [1, 2, 3, 4].filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
// reduce β turns a list into a single value
const total = [1, 2, 3].reduce((sum, x) => sum + x, 0);
console.log(total); // 6Real example: transform API data
const data = [
{ title: "Post 1", likes: 150 },
{ title: "Post 2", likes: 50 },
{ title: "Post 3", likes: 200 }
];
const posts = data
.filter(p => p.likes > 100)
.map(p => ({ title: p.title, stats: p.likes }))
.reduce((acc, p) => acc + p.stats, 0);
console.log(posts); // 350π₯ Worked Example: One Pipeline, All Four Principles
Read this one before you write anything. It is a complete, working revenue calculation that uses every principle so far in the same program: a pure predicate, a pure transformer that copies instead of mutating, a pure reducer, and finally the same three steps wired together with pipe. Every non-obvious line is commented with what it produces.
// A functional pipeline, start to finish.
// Every function here is PURE: the same input always gives the same output,
// and none of them changes anything outside itself. That is what makes the
// whole chain safe to read, test and reorder.
const orders = [
{ id: 1, item: "Keyboard", price: 45, qty: 2, status: "paid" },
{ id: 2, item: "Mouse", price: 18, qty: 1, status: "cancelled" },
{ id: 3, item: "Monitor", price: 210, qty: 1, status: "paid" },
{ id: 4, item: "Cable", price: 6, qty: 4, status: "paid" }
];
// 1) A pure PREDICATE β answers a yes/no question and touches nothing.
const isPaid = order => order.status === "paid";
// 2) A pure TRANSFORMER β returns a NEW object with one extra field.
// The spread copies the existing keys first, so the original is untouched.
const withTotal = order => ({ ...order, total: order.price * order.qty });
// 3) A pure REDUCER β folds a whole list down to a single number.
const sumTotals = (running, order) => running + order.total;
const paid = orders.filter(isPaid); // 3 of the 4 orders survive
const priced = paid.map(withTotal); // each gains a "total" field
const revenue = priced.reduce(sumTotals, 0); // 90 + 210 + 24
console.log("paid orders:", paid.length); // 3
console.log("first total:", priced[0].total); // 90 (45 x 2)
console.log("revenue:", revenue); // 324
// filter and map both returned NEW arrays, so the source data is unchanged.
console.log("original count:", orders.length);
console.log("original has a total?", "total" in orders[0]); // false
// Because every step is pure, you can name the whole pipeline and reuse it.
const pipe = (...fns) => input => fns.reduce((value, fn) => fn(value), input);
const revenueOf = pipe(
list => list.filter(isPaid),
list => list.map(withTotal),
list => list.reduce(sumTotals, 0)
);
console.log("same answer via pipe:", revenueOf(orders));
// β
Expected output:
// paid orders: 3
// first total: 90
// revenue: 324
// original count: 4
// original has a total? false
// same answer via pipe: 324π― Your Turn: Write the Three Pure Functions
Now you write the parts that matter. The pipeline underneath is already wired up β you supply the predicate, the transformer and the reducer. Fill in each ___ and check your result against the expected output at the bottom of the file.
// π― YOUR TURN β supply the three pure functions
// The pipeline below is written for you. Replace each ___ then press Run.
const cart = [
{ name: "Tea", price: 4, qty: 3 },
{ name: "Coffee", price: 9, qty: 1 },
{ name: "Mug", price: 12, qty: 2 }
];
// 1) PURE PREDICATE β keep only lines worth MORE than 10 in total
const isBigLine = line => line.price * line.qty > ___; // π the threshold number
// 2) PURE TRANSFORMER β return a NEW object carrying a "lineTotal" field.
// Spread the original keys in first so nothing is mutated.
const withLineTotal = line => ({ ...___, lineTotal: line.price * line.qty }); // π spread which object?
// 3) PURE REDUCER β add each line's total onto the running figure
const addLineTotal = (running, line) => running + line.___; // π which field holds the line total?
const big = cart.map(withLineTotal).filter(isBigLine);
const total = big.reduce(addLineTotal, 0);
console.log("kept lines:", big.map(l => l.name).join(", "));
console.log("total: " + total);
console.log("cart untouched?", !("lineTotal" in cart[0]));
// β
Expected output:
// kept lines: Tea, Mug
// total: 36
// cart untouched? trueπ₯ Functional Composition Tools
pipe β execute functions left to right:
compose β execute functions right to left:
π₯ Building Full Functional Pipelines
Example: transforming API data into dashboard statistics
const users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 30, active: false },
{ name: "Charlie", age: 35, active: true }
];
const activeOnly = users => users.filter(u => u.active);
const mapNameAndAge = users => users.map(u => ({ name: u.name, age: u.age }));
const averageAge = users =>
users.reduce((acc, u) => acc + u.age, 0) / users.length;
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
const buildStats = pipe(
activeOnly,
mapNameAndAge,
averageAge
);
console.log("Average Age:", buildStats(users)); // 30π₯ FP in State Management (React Example)
React's entire design is functional.
const reducer = (state, action) => {
if (action.type === "increment") return { count: state.count + 1 };
if (action.type === "decrement") return { count: state.count - 1 };
return state;
};
let state = { count: 0 };
state = reducer(state, { type: "increment" });
console.log(state.count); // 1
state = reducer(state, { type: "increment" });
console.log(state.count); // 2The reducer is: pure, stateless, deterministic, and functional. React, Redux, Recoil, and Zustand all rely on FP patterns under the hood.
π₯ Immutability Techniques
Object.freeze (shallow immutability)
const user = Object.freeze({ name: "Boopie", age: 16 });
user.age = 20; // fails silently in non-strict mode
console.log(user.age); // 16Using spread to create copies
const user = { name: "Boopie", age: 16 };
const updated = { ...user, age: 17 };
console.log(user.age); // 16
console.log(updated.age); // 17Array immutability
const arr = [1, 2, 3];
const newArr = [...arr, 4];
console.log(arr); // [1, 2, 3]
console.log(newArr); // [1, 2, 3, 4]π₯ Common FP Mistakes to Avoid
β Treating objects as immutable when they're not
const user = { name: "Boopie" };
const arr = [user];
arr[0].name = "Changed"; // mutation!
console.log(user.name); // "Changed"β Mixing side effects inside pure pipelines
const bad = [1, 2, 3].map(x => {
console.log(x); // impurity!
return x * 2;
});
console.log(bad);FP should simplify code, not increase complexity.
π― Practical Exercise: Data Sanitization Pipeline
A clean FP pipeline for cleaning user input:
const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s+/g, "");
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
const cleanEmail = pipe(trim, toLower, removeSpaces);
const email = cleanEmail(" BOOPIE @ GMAIL .COM ");
console.log(email); // "[email protected]"Most companies use these patterns in authentication systems.
π― Mini-Challenge: Build Your Own Pipe
You have used pipe several times now. Write it yourself, along with the three pure steps it will run. Nothing is filled in β you get the brief and a fixed test drive at the bottom. If your output matches the expected output, you have it.
// π― MINI-CHALLENGE: build your own pipe
//
// Write these four things from scratch. Only the outline is given.
//
// 1. pipe(...fns) -> returns a function that feeds its input through every
// fn in order, left to right, passing each result on.
// 2. onlyActive(users) -> a NEW array holding only users whose active is true
// 3. namesOf(users) -> a NEW array of just the name strings
// 4. shout(names) -> a NEW array with every name in UPPER CASE
//
// None of the four may modify the array it was given.
// Hint for pipe: reduce over the list of functions, carrying the value along.
// your code here
// --- Do not change the code below: this is the test drive ---
const users = [
{ name: "ada", active: true },
{ name: "grace", active: false },
{ name: "linus", active: true }
];
const activeShout = pipe(onlyActive, namesOf, shout);
console.log(activeShout(users).join(" | "));
console.log("original first name still:", users[0].name);
// β
Expected output:
// ADA | LINUS
// original first name still: adaπ― Key Takeaways
- β Pure functions are predictable and testable
- β Immutability prevents unexpected bugs
- β Higher-order functions enable powerful abstractions
- β Currying and partial application increase reusability
- β Recursion replaces loops in functional style
- β Closures provide encapsulation and privacy
- β Memoization optimizes expensive operations
- β Map, filter, reduce replace imperative loops
- β Function composition builds powerful pipelines
- β FP patterns power React, Redux, and modern frameworks
Functional Programming becomes truly powerful when you combine everythingβimmutability, higher-order functions, closures, recursion, currying, composition, and pure data transformationβinto complete, real-world systems. This approach is used in scalable applications, backend APIs, data-processing tasks, rendering engines, and modern frameworks like React, Next.js, Node.js, and Deno.
Practice quiz
What are the two defining properties of a pure function?
- It is fast and short
- It uses const and arrow syntax
- Same input gives same output, and it has no side effects
- It returns a Promise
Answer: Same input gives same output, and it has no side effects. A pure function always returns the same output for the same input and causes zero side effects.
Instead of mutating data, functional programming prefers to...
- Create new copies
- Delete the original
- Use global variables
- Skip the update
Answer: Create new copies. FP avoids changing existing data and instead returns new copies, e.g. with spread.
What does compose(square, double)(3) output, where double = x => x*2 and square = x => x*x?
- 12
- 18
- 9
- 36
Answer: 36. compose(f,g)(x) = f(g(x)): double(3)=6, then square(6)=36.
A higher-order function is one that...
- Runs at a higher priority
- Takes a function as an argument or returns a function
- Has more than 3 parameters
- Cannot be pure
Answer: Takes a function as an argument or returns a function. HOFs either take another function as an argument or return a function.
What does the currying example add(5)(10) return?
- 15
- 50
- 510
- Error
Answer: 15. add(a) returns a function that adds b, so add(5)(10) is 5 + 10 = 15.
What does pipe(x => x + 5, x => x * 3, x => `Value: ${x}`)(10) produce?
- "Value: 35"
- "Value: 30"
- "Value: 45"
- 45
Answer: "Value: 45". pipe runs left to right: 10+5=15, 15*3=45, then formats to "Value: 45".
Object.freeze provides what kind of immutability?
- Deep immutability
- Shallow immutability
- No immutability
- Type immutability
Answer: Shallow immutability. Object.freeze is shallow; nested objects can still be mutated.
What does memoization do?
- Deletes cached values
- Runs functions in parallel
- Mutates the input
- Stores results of expensive calls so they aren't recalculated
Answer: Stores results of expensive calls so they aren't recalculated. Memoization caches results so repeated calls with the same input return instantly.
In the reducer example, after two increment actions from { count: 0 }, what is state.count?
- 0
- 2
- 1
- undefined
Answer: 2. Each increment adds 1, so two increments give count 2.
Which set of methods replaces imperative loops in declarative data flow?
- push, pop, shift
- for, while, do
- map, filter, reduce
- get, set, has
Answer: map, filter, reduce. map, filter, and reduce transform data declaratively instead of using loops.
Continue this course
- Previous: Prototype Chain & Advanced OOP Patterns
- Next: Higher-Order Functions & Currying β Pass, return, and partially apply functions to build powerful abstractions
- Quick reference: JavaScript cheat sheet