Higher Order Functions Generalizing Code

Why do we have Higher-Order Functions? It starts with a simple principle: Don't Repeat Yourself (DRY).

April 23, 20268 min read1 / 3

I've realized that the jump from being a junior developer to a senior one often happens when you stop thinking about what your code does and start thinking about how to make it reusable. In JavaScript, the ultimate tool for reusability is the Higher-Order Function. If you haven't read about the Thread of Execution and Call Stack yet, that's the foundation you'll need before this clicks fully.

The Essentials

  1. DRY Principle: "Don't Repeat Yourself." If you find yourself writing the same logic with minor changes, it's time to generalize.
  2. Callback Function: A function that is passed into another function as an argument.
  3. Higher-Order Function: A function that takes a function as an input or returns a function as an output.
  4. Generalization: Moving from hardcoded data to parameters, and then from hardcoded logic to callback functions.

The Pain of Repetition (Breaking DRY)

Imagine I need to take an array and do something to every element. I might start by writing a function to multiply every number by two:

JavaScript
function copyArrayAndMultiplyBy2(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] * 2); } return output; }

Then, a few minutes later, I need to divide by two. So I write another one:

JavaScript
function copyArrayAndDivideBy2(array) { const output = []; for (let i = 0; i < array.length; i++) { output.push(array[i] / 2); } return output; }

I'm breaking the DRY principle. The only thing changing is a tiny bit of logic: * 2 vs / 2. Everything else--the array creation, the loop, the return--is identical. This is where I start feeling the "pain" of repetition.

Generalizing Functionality

We already know how to generalize data using parameters. Instead of hardcoding 10 * 10, we use square(num) where num is a placeholder.

Higher-Order Functions allow us to do the same for functionality. We leave a "TBD" (To Be Determined) placeholder for the code itself.

JavaScript
function copyArrayAndManipulate(array, instructions) { const output = []; for (let i = 0; i < array.length; i++) { output.push(instructions(array[i])); } return output; } function multiplyBy2(n) { return n * 2; } const result = copyArrayAndManipulate([1, 2, 3], multiplyBy2);

In this model, copyArrayAndManipulate is the Higher-Order Function (the boss), and multiplyBy2 is the Callback Function (the worker).

Visualizing the Interaction

When I run copyArrayAndManipulate, I'm passing a link to the multiplyBy2 code into the instructions parameter. Inside the loop, I'm essentially saying: "Take the current number and run the instructions I gave you on it."

JavaScript Execution Engine
Thread of Execution
1function copyArrayAndManipulate(array, instructions) {
2 const output = [];
3 for (let i = 0; i < array.length; i++) {
4 output.push(instructions(array[i]));
5 }
6 return output;
7}
8function multiplyBy2(n) { return n * 2; }
9const result = copyArrayAndManipulate([1, 2, 3], multiplyBy2);

Step 1:Global memory now stores both the Higher-Order Function and the Callback function.

Memory
copyArrayAndManipulatef
multiplyBy2f
Call Stack
Global
Bottom of Stack

Why This Matters

By decoupling the traversal (looping through the array) from the transformation (the math), I’ve made my code more declarative. I can now change the behavior of my program just by swapping out the callback, without ever touching the logic of copyArrayAndManipulate.

How is this possible? (First-Class Objects)

I used to wonder: how am I even able to pass a function as an input? In many languages, this violates the very idea of what a function is. But in JavaScript, functions are first-class objects.

They have all the properties of an object (plus the "superpower" of being callable). This means they can coexist with any other data type:

  • They can be assigned to variables.
  • They can be properties on objects (known as methods).
  • They can be passed as arguments.
  • They can be returned from other functions.

The Evolution of the Callback

While I often use the function keyword for teaching--because it explicitly screams "I am saving code"--modern JavaScript has moved toward the Arrow Function syntax. It’s not just about typing less; it’s about legibility.

Look at how these four versions are identical under the hood:

JavaScript
// 1. Classic Function Declaration function multiplyBy2(n) { return n * 2; } // 2. Constant Assignment const multiplyBy2 = (n) => { return n * 2; }; // 3. Implicit Return const multiplyBy2 = (n) => n * 2; // 4. Anonymous Arrow (Passed directly) copyArrayAndManipulate([1, 2, 3], n => n * 2);

The Anonymous Trace

When we pass n => n * 2 directly, we aren't even giving it a name in global memory. We are just throwing the code block straight into the Higher-Order Function.

JavaScript Execution Engine
Thread of Execution
1function copyArrayAndManipulate(array, instructions) {
2 const output = [];
3 for (let i = 0; i < array.length; i++) {
4 output.push(instructions(array[i]));
5 }
6 return output;
7}
8const result = copyArrayAndManipulate([1, 2], n => n * 2);

Step 1:We call the HOF. Note that the callback 'n => n * 2' exists only as a piece of code we're passing in.

Memory
copyArrayAndManipulatef
Call Stack
Global
Bottom of Stack

The "Map" Standard

This pattern is so important that JavaScript has a built-in version of our copyArrayAndManipulate called .map(). It is the professional standard for "taking data and transforming it without changing the original."

But as I discovered, not all built-in methods are created equal. Some of the oldest tools in the JavaScript shed have a darker side--they don't just return new data; they mutate the original. In the next part, we'll look at why that’s a "crime" against clean code and how the latest JavaScript features finally fixed it.

Technical Glossary

To keep our communication precise, here are a few terms we’ve introduced:

  • Action Dispatcher: Another name for a callback that triggers a specific behavior inside a Higher-Order Function.
  • Lambda Function: A function passed in without a name (anonymous), often used in languages like Python but synonymous with our anonymous arrow functions.
  • Imperative: Code that focuses on how to do something (the step-by-step for-loop).
  • Declarative: Code that focuses on what to do (e.g., data.map(multiplyBy2)).

Further Reading and Watching

Practice what you just read.

Generalizing Functionality
1 exercise