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

πŸ’‘ 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:

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); // 5
function 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); // 20

This avoids unexpected bugs, especially in UI frameworks like React.

πŸ”₯ Core Principle 3: First-Class & Higher-Order Functions

function applyTwice(fn, value) {
  return fn(fn(value));
}

const double = x => x * 2;

console.log(applyTwice(double, 5)); // 20

This 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)); // 15

This technique is used by:

πŸ”₯ 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

πŸ”₯ 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

πŸ”₯ 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

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

πŸ”₯ 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); // 6

Real 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); // 2

The 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); // 16

Using spread to create copies

const user = { name: "Boopie", age: 16 };
const updated = { ...user, age: 17 };

console.log(user.age); // 16
console.log(updated.age); // 17

Array 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

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