JavaScript and TypeScript course Β· Module 12: Functional Programming

What is Functional Programming?

7 min read
In this lesson6

The morning report from the raptor enclosure says: 12 animals. The evening one, generated by the same function from the same data, says: 13. Nothing hatched, nothing escaped - some function quietly changed a shared counter. Bugs like this cannot be reproduced on demand, and functional programming pulls the ground out from under them.

Imagine a laboratory in Jurassic Park where every experiment on dinosaur DNA always produces the same result given the same input data. There are no side effects - no dinosaur escapes, no security system is accidentally disabled. This is exactly how functional programming (FP) works - it's a paradigm where code behaves like a controlled scientific experiment.

What You Will Learn

  • tell pure functions apart from impure ones,
  • process arrays of samples with map, filter and reduce,
  • assemble functions into processing lines with pipe and compose,
  • configure functions in stages and lock data away in closures,
  • walk the species tree with recursion and change data without mutation,
  • handle missing data and errors with Maybe and Either,
  • build a functional security reporting system for the park.

Functional vs Imperative Paradigm

In imperative programming, we tell the computer how to do something step by step. In functional programming, we describe what we want to achieve, using functions as the fundamental building blocks.

The task: pick the carnivores out of a list of species. First a classic loop, then the functional version:

1// Imperative approach - step by step
2const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops', 'Stegosaurus'];
3const carnivores = [];
4for (let i = 0; i < dinosaurs.length; i++) {
5  if (dinosaurs[i] === 'T-Rex' || dinosaurs[i] === 'Velociraptor') {
6    carnivores.push(dinosaurs[i]);
7  }
8}
9
10// Functional approach - declarative
11const isCarnivore = (name) => ['T-Rex', 'Velociraptor'].includes(name);
12const carnivoresFP = dinosaurs.filter(isCarnivore);

Both versions produce ['T-Rex', 'Velociraptor']. In the first one we look after the counter i, the loop condition and appending results with push ourselves. In the second we only name the rule: isCarnivore uses includes to check whether a name is on the list of carnivores, and we hand the walk through the array over to filter, which returns a new array with the matching elements. What did not change? The dinosaurs array stays untouched in both versions.

Pure Functions

A pure function is a controlled experiment - given the same input data, it always returns the same result and causes no side effects.

Rules of a Pure Function:

  1. Determinism - same input = same output, always
  2. No side effects - does not modify anything outside its scope

The first function below calculates a danger level from its three arguments alone. The second does something that looks innocent: along the way, it bumps a counter declared outside of it.

1// PURE FUNCTION - always the same result
2function calculateDangerLevel(weight, speed, aggression) {
3  return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
4}
5
6// The same function called 1000 times with the same arguments
7// will always return the same result
8calculateDangerLevel(8000, 40, 95); // always 2450
9calculateDangerLevel(8000, 40, 95); // always 2450
10
11// IMPURE FUNCTION - modifies external data
12let totalDinosaurs = 0;
13function addDinosaur(name) {
14  totalDinosaurs++; // side effect - modifies external variable!
15  return { name, id: totalDinosaurs };
16}
17// Each call produces a different result, even with the same argument
18addDinosaur('Rex'); // { name: 'Rex', id: 1 }
19addDinosaur('Rex'); // { name: 'Rex', id: 2 } - different result!

The call calculateDangerLevel(8000, 40, 95) is 2400 + 12 + 38, which makes 2450 - today, tomorrow and on the thousandth repetition. addDinosaur is different: the same argument 'Rex' gives two different results, because the function depends on the variable totalDinosaurs, which it changes itself. That is the mechanism behind the bug in the morning report.

A side effect is any change to the world outside the function: modifying an external variable or an object passed in, writing to the console, a request to a server, changing the page content. Determinism, in turn, is broken by any source of variability, such as randomness:

1// Impure: a side effect in the form of a console entry
2function logSample(id) {
3  console.log('Sample:', id);
4  return id;
5}
6
7// Impure: the result depends on chance, not on the arguments
8function randomZone() {
9  return Math.random() > 0.5 ? 'A' : 'B';
10}

Neither of them touches the park's data, and yet both are impure: logSample leaves a trace in the console, and randomZone, with no arguments at all, returns 'A' one time and 'B' another. Any function that reads the time through Date.now() is impure in the same way.

Immutability

Immutability is the principle that once created, data is never changed. Instead of modifying an existing object, we create a new copy with changes. It's like creating a new DNA sequence instead of modifying an existing one - safer and more predictable.

Compare two ways of recording that Rex has been injured:

1// MUTATION - dangerous (modifying the original object)
2const dinosaur = { name: 'Rex', health: 100 };
3dinosaur.health = 80; // The original object has been changed!
4
5// IMMUTABILITY - safe (new object)
6const dinosaurSafe = { name: 'Rex', health: 100 };
7const injuredDinosaur = { ...dinosaurSafe, health: 80 };
8// dinosaurSafe still has health: 100 (unchanged)
9// injuredDinosaur has health: 80 (new object)

The spread operator (...) copies all fields into a new object, and health: 80 written after it overrides one field of the copy. Notice also that const did not protect the dinosaur object from being changed: it blocks reassigning the variable, not the contents of the object. We will come back to this difference in the lesson on immutability patterns.

Functions as First-Class Citizens

In JavaScript, functions are first-class citizens - you can assign them to variables, pass them as arguments, and return them from other functions.

First, the first two options: we store a function in a constant and then pass it to filter, the way a sample is passed between workstations:

1// Function assigned to a variable
2const classify = (dino) => dino.diet === 'carnivore' ? 'dangerous' : 'safe';
3
4// Function as an argument
5const dinosaurs = [
6  { name: 'Rex', diet: 'carnivore' },
7  { name: 'Brachio', diet: 'herbivore' },
8];
9const dangerous = dinosaurs.filter((dino) => classify(dino) === 'dangerous');

Assigning classify does not run anything - it is a recipe that filter will call for each dinosaur. Only Rex ends up in the dangerous array.

The third option is the most interesting, because a function can build and return a new function:

1// Function returning a function
2function createValidator(minWeight) {
3  return (dino) => dino.weight >= minWeight;
4}
5const isHeavyDino = createValidator(5000);
6isHeavyDino({ weight: 8000 }); // true
7isHeavyDino({ weight: 200 });  // false

createValidator(5000) produces a ready-made validator that remembers the minWeight threshold, even though the factory call finished long ago. Functions that take or return other functions are called higher-order functions, and the mechanism for remembering variables is a closure - we will develop both topics in the coming lessons.

Why Bother?

You test a pure function without preparing the world around it: you pass arguments and compare the result - remember Jest from the previous location? You can also safely remember its result or chain it with other functions. You will not write a program entirely without side effects, though, because someone has to display the report. That is why I recommend Dr. Rex's rule to you: keep effects at the edges of the program (loading data at the start, console.log and saving at the end), and write the logic in the middle as pure functions.

In the editor below, change the arguments and check which functions behave like a controlled experiment.

Functional programming comes down to a single promise: the same input always yields the same result, no specimen is quietly altered behind your back, and functions travel through your code as freely as any other value in the laboratory.

Code for this lesson: index.js
1// Functional programming - Introduction
2console.log("=== Jurassic Park - Pure Function Laboratory ===\n");
3
4// EXAMPLE 1: Pure vs impure function
5
6// PURE FUNCTION - always the same result, no side effects
7function calculateDangerLevel(weight, speed, aggression) {
8  return (weight * 0.3) + (speed * 0.3) + (aggression * 0.4);
9}
10
11console.log("--- Pure functions ---");
12console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
13console.log("T-Rex danger:", calculateDangerLevel(8000, 40, 95));
14console.log("Identical results! A pure function is deterministic.\n");
15
16// IMPURE FUNCTION - side effects
17let totalRegistered = 0;
18function registerDinosaurImpure(name) {
19  totalRegistered++; // Side effect!
20  return { name, id: totalRegistered };
21}
22
23console.log("--- Impure example ---");
24console.log("Rex:", registerDinosaurImpure("Rex"));
25console.log("Rex:", registerDinosaurImpure("Rex"));
26console.log("Different results! The function depends on external state.\n");
27
28// EXAMPLE 2: Immutability
29console.log("--- Immutability ---");
30const dinosaur = { name: "Rex", health: 100 };
31const injuredDino = { ...dinosaur, health: 80 };
32
33console.log("Original:", dinosaur);
34console.log("New object:", injuredDino);
35console.log("Original unchanged:", dinosaur.health === 100);
36
37// EXAMPLE 3: Functions as values
38console.log("\n--- Functions as values ---");
39const classify = (dino) => dino.diet === "carnivore" ? "dangerous" : "safe";
40
41const dinos = [
42  { name: "Rex", diet: "carnivore" },
43  { name: "Brachio", diet: "herbivore" },
44  { name: "Raptor", diet: "carnivore" },
45];
46
47const dangerous = dinos.filter((d) => classify(d) === "dangerous");
48console.log("Dangerous:", dangerous.map((d) => d.name));
49
50// EXAMPLE 4: A function returning a function
51function createWeightFilter(minWeight) {
52  return (dino) => dino.weight >= minWeight;
53}
54
55const isHeavy = createWeightFilter(5000);
56console.log("\nIs Rex (8000kg) heavy?", isHeavy({ weight: 8000 }));
57console.log("Is Raptor (150kg) heavy?", isHeavy({ weight: 150 }));

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. Which of the following functions is a pure function?

Useful articles