I don't often talk about the basics, but everyone needs to hear them. Let's take a stroll down the fun side of programming.
Higher-Order Functions
... are functions that act on a collection. They shouldn't change the collection, but they will return a result that can further be acted upon. They typically take a function as an argument, which is called for each element in the collection.
I like to use JavaScript because it's universal. So let's set up a simple array: a collection of simple objects.
const arr = [
{a: 1},
{a: 2},
{a: 3},
{a: 4},
{a: 5}
];
Your basic higher-order function work like this:
const valuesOnly = arr.map((o) => o.a); // [1,2,3,4,5]
const odds = arr.filter((o) => o.a % 2); // [{a:1},{a:3},{a:5}]
const sum = arr.reduce((acc, o) => acc + o.a, 0); // 15
- Map can change the shape of the collection (but not the size)
- Filter can change the size of the array (but not the shape)
- Reduce can change both shape and size, returning an entirely different result
It gets better when we chain these together
const result = arr.map((o) => o.a)
.filter((a) => a % 2)
.reduce((acc, a) => acc + a, 0);
console.log(result); // 9
Map, filter, and reduce are arguably the most helpful higher-order functions that are universally available, although different languages or platforms may change the wording. For example, in C#, you would "Select" instead of map, "Where" instead of filter, and "Aggregate" instead of reduce - the input and output are relatively the same but C# likes to think it has a variation of SQL inside it called "Linq."
Other higher-order functions on JavaScript arrays include entries, every, find and findIndex, flatMap, forEach, some, and toSorted.
Write Your Own
I can show you how to use these function, but in order to actually understand them, you have to write your own! It's super easy and honestly kind of fun.
Set up a test file - I usually make a test.js somewhere. Put a console.log("Test"); at the top so you know it works, save it, then execute it on the command line with node test. If you've never done this, I highly recommend it. This simple test file setup is the easiest way to get into real programming.
Our versions won't be quite as powerful as the platform native versions, but trust me, this is worthwhile.
Start with map!
The array.map function was added in 2009 - almost 15 years after JavaScript was created! Before the official array.map() function, we had to write our own. So let's do that.
Start by defining it - we're going to use this later on, so we can't use a contextless arrow function.
const myMap = function(arr, fn) { /* ??? */ };
Now we're ready to think. What does the map method even do? This is why we're here.
If you're here to learn, this is where you pause and write your own.
Map creates a new array. This is important for immutability - we use more memory but our structures stay clean, it avoids errors (like if someone had a reference to the array and then we changed it), and it should be faster since we're not making changes to shared objects. In some platforms, this allows us to use an even-faster readonly collection.
map loops over the array, does something, and returns a result. Best yet, map doesn't actually have to know what it's doing to the array, it just has to call the inner function (fn) that gets passed in. fn will return the result, and our map function returns the new array with new result values.
So we start by creating that array - we even know how big to make it. Then we loop. Then we call fn for each element and push
OK enough hints, my solution is below. No cheating.
const myMap = function(arr, fn) {
const newArr = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
newArr[i] = fn(arr[i]);
}
return newArr;
};
const result = myMap(arr, (o)=>o.a);
console.log(result);
Some notes about my solution:
- The Array constructor lets you pass in an initial size, which helps with performance.
- Please use a normal loop, not a
.forEachloop, which is just another higher-order function. The regular for loop is the usually the fastest one, however afor..inorfor..ofloop is fine. - The argument function -
(o)=>o.ajust extracts the inner valueafrom the objects in the array.
The next thing to do is to hoist it up into the array object just like today's modern map method does. JavaScript makes it pretty easy.
Array.prototype.myMap = myMap;
Without changes, we would call it like this:
arr.myMap(arr, (o)=>o.a);
This lacks the elegance of the native map method because it has the extra argument arr. Fixing this isn't difficult. When we attach our method to the prototype Array, we can then use the keyword this which refers to whichever array we are working on. Our new map method would look like this:
const myMap = function(fn) {
const newArr = new Array(this.length);
for (let i = 0; i < this.length; i++) {
newArr[i] = fn(this[i]);
}
return newArr;
};
Array.prototype.myMap = myMap;
const result = arr.myMap((o) => o.a);
console.log(result);
Let's Try filter!
Filter is essentially the same formula. Try to write your own before you see mine.
const myFilter = function(fn) { /* ??? */ };
Make sure you use this instead of an array that we won't pass in, and set it in the Array prototype so that it's available on every array we use. Start by copying & pasting the code you wrote for myMap and instead of just returning the result of fn, you will use it to filter the array.
Write your own and then check it against mine
const myFilter = function(fn) {
const newArr = []
for (let i = 0; i < this.length; i++) {
if (fn(this[i])) {
newArr.push(this[i]);
}
}
return newArr;
};
Array.prototype.myFilter = myFilter;
const result = arr.myFilter((o) => o.a % 2);
console.log(result);
- You won't know the final length of the new array ahead of time.
- The method is still trivial.
I tell middle & high school kids this all the time - there is no magic in computers, just things you haven't learned yet.
The Final Boss, reduce!
On its surface, reduce is the most complex method of the three. In reality it's not terribly complex. I'll start us off, you write your own, test it, then compare it with the code I write below.
const myReduce = function(fn, initialValue) { /* ??? */ };
Array.prototype.myReduce = myReduce;
const result = arr.myReduce((acc, o) => acc + o.a, 0);
console.log(result);
reduce takes in an additional argument, the initial value. It's easy to miss as the second argument after the function. For our simple sum function we are just going to add the object .a property's values: (acc,o)=>acc+o.a and provide an initial value 0.
Reduce doesn't usually return another array. You can, but it's not the simple use case we will do today.
Ready?
const myReduce = function(fn, initialValue) {
let acc = initialValue; // acc = accumulator
for (let i = 0; i < this.length; i++) {
acc = fn(acc, this[i]);
}
return acc;
};
Array.prototype.myReduce = myReduce;
const result = arr.myReduce((acc, o) => acc + o.a, 0);
console.log(result);
Play with it a bit and make sure you understand how this really works. If it helps, take off the arrow functions and make sure you return an actual value from that inner function. The most common error is for the inner function to not return a value, so then the next loop iteration has undefined for the accumulator value.
Once you understand these, you can start to build your own.
Building Custom Higher-Order Functions
Now that you can do it yourself, stretch out with it. Think outside of the standard boxes. These are wings that not every programmer has. This opens up the path to create your own zip and unzip methods to combine and detach data types. You can build data into windowed chunks, create distinct filtering that works everywhere, or an easy compact method to pop out null values.
You can make more nuanced methods that are highly specific to your application. You could mash your Map and Filter methods together to reduce the number of loops. For example, arr.mapFilter(mapFn, fltFn) has promise, right?
Of course, this isn't the most normal way to go. You could build something that's very difficult to understand. If you overdo it people will start to say you are monkeypatching which could lead to unintended side effects. On the other hand, this is a well-documented way to write JavaScript. It's pretty fun and powerful. I think it's worth experimenting with.