Fancy forEach with functional programming in JavaScript
I’m not a functional programming guru by any means, but I have a passing curiosity and I’ve been trying to learn more about it. I recently dealt with some annoying, repetitive code at work[1] by applying some functional programming concepts.
Here’s a boiled down example that’s similar to what I ran into.
First, suppose you have an API that gives you a list of TV shows. We’ll store them in an array like this.
1const tvShows = [2 { id: 100, name: 'Better Call Saul' },3 { id: 101, name: 'Fringe' },4 { id: 102, name: 'Stargate SG-1' },5 { id: 103, name: 'Ted Lasso' },6 { id: 104, name: 'The Office' }7];
Now assume that a separate API call gives us a user’s watch list. It’s an array of IDs corresponding to TV shows they would like to watch at some point.
1const watchList = [101, 102, 104, 105];
It is just an array of numbers. But look again and you may notice a problem. We received the ID 105
in our watchList
, but we don’t have a TV show in our dataset with that ID.
Now, in a real world app you would probably need to deal with this discrepancy in some kind of nice way. For our purposes, however, we’re following the rule that if we receive an ID that our program doesn’t know about, we can just ignore it.
With that in mind, let’s think about how we would retrieve a list of TV show names, based on the user’s watch list. Here’s one way we could do it.
1const names = [];2 3for (const id of watchList) {4 const show = tvShows.find(s => s.id === id);5 6 if (show) {7 names.push(show.name);8 }9}
We’re given the ID, so we can do a lookup based on that, then add the name of the show to the list.
This isn’t terrible, but because we have to deal with potentially nonexistent IDs, we have to check the result of the lookup to make sure it exists before trying to add its name to our list.
We can tighten this up a bit by chaining together a few of JavaScript’s array methods. This results in more concise and FP-friendly code.
1const names = watchList2 .map(id => tvShows.find(s => s.id === id)?.name)3 .filter(s => s);
The Array
object’s map
method[2] returns a new array based on the original. The difference is that it lets us run a function on each item in the array. Whatever we return from that function ends up in the new array.
We’re first mapping the watch list. For each ID, we do a lookup with find
and return the name of the show. We’re taking advantage of JavaScript’s optional chaining feature (notice the ?.
). If the show doesn’t exist, this causes the expression to short circuit and return undefined
.
Finally, we add a quick filter on the end to throw out any undefined
items in our list of names.
This is better. But if we need to do this often, it’s kind of annoying that we have to do the lookup and the filter every time.
There are multiple ways to solve this, of course. For example, it would probably be smart to run this loop one time and store the results in an array for future usage. Let’s assume that there’s an external reason preventing us from doing that.
Instead, let’s make a new function that does the work for us.
1const mapWatchList = fn => watchList2 .map(id => tvShows.find(s => s.id === id))3 .filter(s => s)4 .map(fn);
In this example, we’ve created a function called mapWatchList
. It receives a function as its only parameter. It then maps the watch list, returning the corresponding show for each ID. It then filters out any instances where the show doesn’t exist. Finally, it maps over the new list of shows using the callback function it received.
This means that we can now access the watch list, not as IDs, but as TV show objects.
1const names = mapWatchList(show => show.name);2console.log(names)3// [ 'Fringe', 'Stargate SG-1', 'The Office' ]
There is one problem, though. While our code does work, it is a bit inefficient. It must loop over all of the TV shows, then loop over the results in order to filter out nonexistent shows. Then, finally, loop over the results of that, invoking the callback function for each item. That’s three loops. Ideally, we’d only loop through the TV show list one time.
Let’s refactor our mapWatchList
function so that it works the same way but only performs one loop over the list. For this, we will be returning to our performant friend,[3] the for...of
loop.
1const mapWatchList = fn => { 2 const mappedShows = []; 3 4 for (const id of watchList) { 5 const show = tvShows.find(s => s.id === id); 6 7 if (show) { 8 mappedShows.push(fn(show)); 9 }10 }11 12 return mappedShows;13}
Again we are accepting a function as the only parameter. Remember, the goal of this function is to use it like a map. That means it should invoke our callback function on each item of the watch list and use the return value of the callback as an item in the new array.
We create an empty array that will hold all of our mapped shows. Then we iterate through the watchList
. For each ID, we do a lookup to get the full show object. If we find one, we invoke the callback function and store the result in the mappedShows
array.
After the loop is complete, we return the results.
This works great, and we could stop here.
But…
It’s kind of not great that we are accessing variables that live outside our function (tvShows
and watchList
). And what if our application needs to do the same thing but for other entities besides TV shows?
For our final trick, let’s go up one more level and make a function that can make the mapWatchList
function.
To do this, we need to think generically. Instead of a list of TV shows and list of TV show IDs, let’s think of it as being a list
and ids
. Now in our case we are matching based on 'id'
, but let’s make it such that you can match on whatever primitive value you want. We will call that the identifier
, and the consumer of our function will need to pass that in.
So we’ll ask for these three things:
list
: this is the full array of objectsids
: this is an array of identifiersidentifier
: this is a string, such as'id'
, that we will use for the lookup
Here we goooooo (Mario voice):
1const mapThingsWithIds = (list, ids, identifier) => fn => { 2 const mappedThings = []; 3 4 for (const id of ids) { 5 const thing = list.find(s => s[identifier] === id); 6 7 if (thing) { 8 mappedThings.push(fn(thing)); 9 }10 }11 12 return mappedThings;13};
Let’s go from the inside out. We are still returning a function that loops over a list of IDs, does a lookup from a list of objects, then invokes a callback for each item it finds. But rather than access specific arrays, we wrap our function inside another function that accepts the information we need.
We use it like this:
1const mapWatchList = mapThingsWithIds(tvShows, watchList, 'id');2const names = mapWatchList(show => show.name);
If this is breaking your brain, I get it. Creating a function that returns a function that returns a function can get a little trippy to think about. But what we’ve done is make our work reusable and flexible. We are no longer limited to TV shows.
We can use our new function with any other kind of entity, and match using a property other than 'id'
:
1const mapReadingList = mapThingsWithIds(books, readingList, 'isbn');2const titles = mapReadingList(book => book.title);
See? Nice, efficient, and reusable!
But I have a confession to make. In my case, I didn’t go this far. I stopped after making the first helper function. The point of this little tutorial is just to show you what you can do. Even turning a little bit of procedural code into more declarative, functional code can be helpful. This only scratches the surface of interesting things you can do with a functional programming style. If you want more on this, check out these resources:
- Functional programming in JavaScript: A video series on YouTube by Fun Fun Function.
- Functional-Light JavaScript: an ebook by Kyle Simpson. “A uniquely pragmatic approach to explaining core functional programming concepts WITHOUT wading through mathematical notation or heavy terminology.”
Code that I wrote, just to be clear. 😅 ↩︎
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map ↩︎
for
loops are faster than JavaScript’s array methods. https://leanylabs.com/blog/js-forEach-map-reduce-vs-for-for_of/ ↩︎