DEV Community

Samuel Ochaba
Samuel Ochaba

Posted on

Three Programming Superpowers In One Function

You don't need a computer science degree to understand these concepts. Let me show you three of programming's most elegant ideas, one at a time, building up to a powerful combination.


Recursion (A Function That Calls Itself)

What Is It?

Recursion is when a function calls itself to solve a problem. It sounds weird at first, but it's actually how you naturally think about many problems.

Real-Life Example: Russian Nesting Dolls

Imagine you have Russian nesting dolls and want to count how many there are. Here's how you'd think about it:

  1. Open the current doll
  2. Is there a doll inside?
    • If YES: Count that doll (using the same process)
    • If NO: You've reached the smallest doll, you're done

You're using the same process over and over, just on smaller dolls. That's recursion!

Programming Example: Countdown

function countdown(n) {
  if (n === 0) {
    console.log("Blast off!");
    return;
  }
  console.log(n);
  countdown(n - 1);  // The function calls itself!
}

countdown(5);
// Prints: 5, 4, 3, 2, 1, Blast off!
Enter fullscreen mode Exit fullscreen mode

What's happening:

  • countdown(5) prints 5, then calls countdown(4)
  • countdown(4) prints 4, then calls countdown(3)
  • countdown(3) prints 3, then calls countdown(2)
  • countdown(2) prints 2, then calls countdown(1)
  • countdown(1) prints 1, then calls countdown(0)
  • countdown(0) sees we've hit zero (the "base case") and stops

Another Example: Adding Numbers

Let's add all numbers from 1 to n:

function sumUpTo(n) {
  if (n === 1) {
    return 1;  // Base case
  }
  return n + sumUpTo(n - 1);  // Recursive case
}

console.log(sumUpTo(5));  // 15
Enter fullscreen mode Exit fullscreen mode

How it works:

  • sumUpTo(5) = 5 + sumUpTo(4)
  • sumUpTo(4) = 4 + sumUpTo(3)
  • sumUpTo(3) = 3 + sumUpTo(2)
  • sumUpTo(2) = 2 + sumUpTo(1)
  • sumUpTo(1) = 1 (base case!)
  • Now it works backward: 2 + 1 = 3, then 3 + 3 = 6, then 4 + 6 = 10, then 5 + 10 = 15

Key Parts of Recursion

Base Case: When to stop (like reaching the smallest doll or hitting zero)

Recursive Case: The function calling itself with a simpler version of the problem

The Pattern: Big problem → smaller problem → even smaller → ... → simplest problem → build back up with answers


Closure (A Function's Private Memory)

What Is It?

A closure is when a function "remembers" variables from the place where it was created, even after that place is gone. It's like giving a function its own private backpack of data that nobody else can access.

Real-Life Example: A Secret Note

Imagine a spy agency that creates agents:

function createSpy(secretCode) {
  // Each spy gets their own secret code
  return function identifyYourself() {
    console.log("My secret code is: " + secretCode);
  };
}

const agent007 = createSpy("Bond");
const agent008 = createSpy("Bourne");

agent007();  // "My secret code is: Bond"
agent008();  // "My secret code is: Bourne"
Enter fullscreen mode Exit fullscreen mode

Even though createSpy has finished running, each agent still remembers their own secretCode. That's a closure!

Example: A Counter

function makeCounter() {
  let count = 0;  // Private variable

  return function() {
    count++;  // Can access count even though makeCounter is done
    return count;
  };
}

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1());  // 1
console.log(counter1());  // 2
console.log(counter1());  // 3
console.log(counter2());  // 1 (separate counter!)
console.log(counter2());  // 2
Enter fullscreen mode Exit fullscreen mode

Each counter has its own private count variable. You can't access count directly from outside — it's protected inside the closure.

Example: Password Validator

function createPasswordChecker(correctPassword) {
  let attempts = 0;

  return function(guess) {
    attempts++;
    if (guess === correctPassword) {
      return `Correct! You got it in ${attempts} attempts.`;
    } else {
      return `Wrong! Attempt ${attempts}`;
    }
  };
}

const checkMyPassword = createPasswordChecker("secret123");

console.log(checkMyPassword("password"));     // "Wrong! Attempt 1"
console.log(checkMyPassword("hello"));        // "Wrong! Attempt 2"
console.log(checkMyPassword("secret123"));    // "Correct! You got it in 3 attempts."
Enter fullscreen mode Exit fullscreen mode

The returned function remembers both correctPassword and attempts. These variables are private — no one outside can reset attempts or see correctPassword.

Why Closures Matter

  • Data Privacy: Variables are protected inside the closure
  • State Persistence: Data survives between function calls
  • Factory Pattern: Create multiple independent functions with their own data

Caching (Never Do The Same Work Twice)

What Is It?

Caching means saving the result of a calculation so you don't have to do it again. It's like writing down answers to homework problems so you can look them up later instead of recalculating.

Real-Life Example: A Phone Book

Imagine calling a friend. The first time, you have to:

  1. Find the phone book
  2. Look up their name
  3. Find the number
  4. Dial

But your phone is smart — it saves the number. Next time, you just tap their name and call instantly. That's caching!

Example: Expensive Calculation

function slowSquare(n) {
  console.log(`Calculating ${n} squared...`);
  // Imagine this takes a long time
  return n * n;
}

console.log(slowSquare(5));  // "Calculating 5 squared..." → 25
console.log(slowSquare(5));  // "Calculating 5 squared..." → 25 (calculated again!)
console.log(slowSquare(5));  // "Calculating 5 squared..." → 25 (calculated again!)
Enter fullscreen mode Exit fullscreen mode

We calculated the same thing three times! Let's add caching:

function makeSmartSquare() {
  let cache = {};  // Our storage

  return function(n) {
    if (cache[n] !== undefined) {
      console.log(`Found ${n} squared in cache!`);
      return cache[n];
    }

    console.log(`Calculating ${n} squared...`);
    cache[n] = n * n;
    return cache[n];
  };
}

const smartSquare = makeSmartSquare();

console.log(smartSquare(5));  // "Calculating 5 squared..." → 25
console.log(smartSquare(5));  // "Found 5 squared in cache!" → 25
console.log(smartSquare(7));  // "Calculating 7 squared..." → 49
console.log(smartSquare(5));  // "Found 5 squared in cache!" → 25
Enter fullscreen mode Exit fullscreen mode

Now it calculates each number only once!

Example: API Calls

Imagine fetching user data from a server (slow):

function makeUserFetcher() {
  let userCache = {};

  return async function getUser(userId) {
    if (userCache[userId]) {
      console.log("Returning cached user");
      return userCache[userId];
    }

    console.log("Fetching from server...");
    // Simulate server call
    const user = await fetch(`/api/users/${userId}`);
    userCache[userId] = user;
    return user;
  };
}

const getUser = makeUserFetcher();

await getUser(123);  // "Fetching from server..."
await getUser(123);  // "Returning cached user" (instant!)
await getUser(456);  // "Fetching from server..."
await getUser(123);  // "Returning cached user" (still instant!)
Enter fullscreen mode Exit fullscreen mode

The Caching Pattern

  1. Check: Is the answer already saved?
  2. Return: If yes, return it immediately
  3. Calculate: If no, do the work
  4. Save: Store the answer
  5. Return: Give back the answer

Putting It All Together: The Fibonacci Example

Now that you understand each concept, let's see them work together to create something powerful.

The Problem: Fibonacci Numbers

The Fibonacci sequence is a pattern where each number is the sum of the two before it:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55...
Enter fullscreen mode Exit fullscreen mode

To find the 6th number: add the 5th and 4th

To find the 5th number: add the 4th and 3rd

And so on...

The Code

function fibMaker() {
  let cache = new Map();

  return function fib(n) {
    if (cache.has(n)) {
      return cache.get(n);
    } else {
      if (n === 1 || n === 2) {
        cache.set(n, 1);
        return 1;
      } else {
        cache.set(n, fib(n - 1) + fib(n - 2));
        return fib(n - 1) + fib(n - 2);
      }
    }
  };
}

const fib = fibMaker();
console.log(fib(77));  // 5527939700884757 (instantly!)
Enter fullscreen mode Exit fullscreen mode

Breaking It Down

The Closure:

function fibMaker() {
  let cache = new Map();  // This is the backpack
  return function fib(n) {
    // This function carries 'cache' everywhere
  };
}
Enter fullscreen mode Exit fullscreen mode

The cache variable is created once and remembered by the fib function forever. It's private — only fib can access it.

The Caching:

if (cache.has(n)) {
  return cache.get(n);  // Found it! Return saved answer
}
// ... later ...
cache.set(n, fib(n - 1) + fib(n - 2));  // Save the answer
Enter fullscreen mode Exit fullscreen mode

Before calculating, check if we've done this before. After calculating, save the result.

The Recursion:

fib(n - 1) + fib(n - 2)  // The function calls itself twice!
Enter fullscreen mode Exit fullscreen mode

To find fib(5), we need fib(4) and fib(3). To find those, we call fib again with smaller numbers.

Watching It Work: fib(6)

Let's trace what happens step by step:

fib(6) needs fib(5) and fib(4)
  ↓
  fib(5) needs fib(4) and fib(3)
    ↓
    fib(4) needs fib(3) and fib(2)
      ↓
      fib(3) needs fib(2) and fib(1)
        ↓
        fib(2) = 1 ✅ [CACHE: {2: 1}]
        fib(1) = 1 ✅ [CACHE: {2: 1, 1: 1}]
      ↑
      fib(3) = 2 ✅ [CACHE: {2: 1, 1: 1, 3: 2}]
      fib(2) = 1 (from cache! ⚡)
    ↑
    fib(4) = 3 ✅ [CACHE: {2: 1, 1: 1, 3: 2, 4: 3}]
    fib(3) = 2 (from cache! ⚡)
  ↑
  fib(5) = 5 ✅ [CACHE: {2: 1, 1: 1, 3: 2, 4: 3, 5: 5}]
  fib(4) = 3 (from cache! ⚡)
↑
fib(6) = 8 ✅
Enter fullscreen mode Exit fullscreen mode

Notice how fib(3), fib(4), and fib(2) are requested multiple times, but only calculated once!

The Incredible Performance Difference

Without caching:

  • fib(10): 177 calculations
  • fib(20): 21,891 calculations
  • fib(30): 2,692,537 calculations
  • fib(40): 331,160,281 calculations
  • fib(50): Your computer would freeze
  • fib(77): Would take longer than the age of the universe

With caching:

  • fib(10): 10 calculations
  • fib(20): 20 calculations
  • fib(77): 77 calculations (instant result!)

That's the power of combining recursion, closures, and caching!


Try It Yourself

Run this code with console logs to see the magic:

function fibMaker() {
  let cache = new Map();
  let calculations = 0;
  let cacheHits = 0;

  return function fib(n) {
    if (cache.has(n)) {
      cacheHits++;
      console.log(`✓ Found fib(${n}) in cache = ${cache.get(n)}`);
      return cache.get(n);
    } else {
      calculations++;
      console.log(`→ Calculating fib(${n})...`);

      if (n === 1 || n === 2) {
        cache.set(n, 1);
        return 1;
      } else {
        let result = fib(n - 1) + fib(n - 2);
        cache.set(n, result);
        console.log(`← fib(${n}) = ${result}`);
        return result;
      }
    }
  };
}

const fib = fibMaker();
console.log("Final result:", fib(10));
Enter fullscreen mode Exit fullscreen mode

Why This Matters

You just learned three fundamental concepts that power modern software:

Recursion solves problems by breaking them into smaller versions of themselves. Used in: file systems, search algorithms, rendering UI components, parsing code.

Closures give functions private, persistent data. Used in: React hooks, event handlers, API wrappers, module patterns.

Caching eliminates redundant work. Used in: web browsers, databases, CDNs, every high-performance application.

The Big Picture

Programming isn't about memorizing syntax. It's about understanding patterns that make your code elegant and efficient. You just learned three of the most powerful patterns in computer science.

Top comments (0)