You’re probably familiar with the standard schlew of Array methods in JavaScript. We’re all fond of timeless classics such as forEach
, map
, and even filter
. However, there is one method that has been hiding under the radar (at least for me) for quite a while now. It’s one that functional-reactive-programming tends to use somewhat often, and that method is reduce
.
Array.reduce
does what it sounds like. It reduces an array of items to a single value and returns it. Most of the examples out there tend to deal with math:
[1,2,3].reduce(function add(prevNum, currNum) {
return prevNum + currNum;
});
// Returns 6... wow
The API is fairly straightforward. Reduce accepts two values: A callback
function
that gets called for each array element (left to right), and an initialValue
argument, which can be whatever the developer wishes. Let’s take a look at what the callback function get’s invoked with:
previousValue
: The value returned from the last call of our function, or an optional initalValue
if it’s on the first call. In the above codeblock, this was the prevNumber
.currentValue
: The current value of the element being iterated over. Think array[i]
if you’re used to standard for
loops. Above, this was currNumber
.index
: The current index that the loop is at. Obviously numeric.array
: The array being iterated over. In our example, this is [1,2,3]
.Here’s a fun example using the initialValue
described above:
['B','a','t','m','a','n'].reduce(function makeBatman(prevLetter, currLetter){
return prevLetter + currLetter;
}, 'NaNaNaNaNaNaNaNaNaNaNa ');
// Print: "NaNaNaNaNaNaNaNaNaNaNa Batman"...wow
Wow, that’s pretty neat, but still not something useful day-to-day.
Story time. As you may know, I’m the author of Sickmerge, a handy way to do 3-way merging in the browser when you have a git conflict. In version 2, I’m adding the ability to load up ALL of your conflicted files into the browser to merge them all in one whack. This involves running a git command under the hood to get all conflicted files, which subsequently returns an array of file-paths that I’d like to represent in a JSON form (so we can build a tree-view UI later).
And therein lies the use-case for reduce
‘s usefuleness: Given an array of file-paths, merge them into a simple object that represents the folder hierarchy. Let’s draw out an example here:
// Our filePaths
var filePaths = ['src/lib/git.js','src/lib/server.js','externs/jquery.js','index.js'];
// What we want back
{
'index.js': 'file',
'src': {
'lib': {
'git.js': 'file',
'server.js': 'file'
},
},
'externs': {
'jquery.js': 'file'
}
}
While this seems really simple, it’s actually a HUGE pain to implement without reduce
. I’m going to throw down the solution now, and we can break it down:
function Treeify(files) {
var fileTree = {};
if (files instanceof Array === false) {
throw new Error('Expected an Array of file paths, but saw ' + files);
}
function mergePathsIntoFileTree(prevDir, currDir, i, filePath) {
if (i === filePath.length - 1) {
prevDir[currDir] = 'file';
}
if (!prevDir.hasOwnProperty(currDir)) {
prevDir[currDir] = {};
}
return prevDir[currDir];
}
function parseFilePath(filePath) {
var fileLocation = filePath.split('/');
// If file is in root directory, eg 'index.js'
if (fileLocation.length === 1) {
return (fileTree[fileLocation[0]] = 'file');
}
fileLocation.reduce(mergePathsIntoFileTree, fileTree);
}
files.forEach(parseFilePath);
return fileTree;
}
Ok, so in the start of Treeify
we create an empty object fileTree
that we’ll use to build out our file-tree. Next, we create a function mergePathsIntoFileTree
that we’ll pass each file-path into to merge into our fileTree
object. We’ll also define a parseFilePath
that will take the file-path as a string, and parse it out into something more manageable. Lastly, we’ll iterate over each file-path and put each value into parseFilePath
then return the fileTree
when we’re done. Here’s a quick run-down of the flow:
reduce
function!:Let’s talk a bit about parseFilePath
as there is some voodoo inside. This function
takes each file path, which has a format of 'dir/dir/file'
and breaks it up into it’s own array of values. So 'some/path/to/file.js'
becomes ['some','path','to','file.js']
, which indicates our hierarchy in a nicer format. Once that is done, we run the reduce
method on the resulting array, which then calls the mergePathsIntoFileTree
function to handle the reduction. Whew.
Let’s take a look at the first call into reduce
with an example of ['src', 'lib']
:
// Simplified example without the file (just file paths).
var fileTree = {};
['src','lib'].reduce(function mergeIntoFileTree(prevDir, currDir){
// In the first iteration `prevValue` is fileTree, the blank {}.
// Does the {} have a property 'src'?
if (!prevDir.hasOwnProperty(currDir)) {
prevDir[currDir] = {};
}
// In the next call, `prevDir` will now be `src` property of {}
return prevDir[currDir];
}, fileTree);
Herein lies magic! In our initial iteration, we pass an empty {}
into the reduce
call, which kicks us off. Inside that block, we check to see if the {}
has a property src
, and if not we’ll create it and assign it to an empty object. We return the reference to the src
property at the end, so the next iteration automatically starts at that property! This is perfect, since now we’ll want to run the same exact logic, but one level deeper in the object at src
checking to see if lib
is exists in it. If we were to pass in another ['src','lib']
no action would be taken, as the appropriate properties are already there.
Looking back on our example use-case, we wanted to transform ['src/lib/git.js','src/lib/server.js','externs/jquery.js', 'index.js']
into a single Object. After our first run-through of reduce
, we now have an object that looks like:
// After first path has been merged in
{
'src': {
'lib': {
`git.js`: `git.js`
}
}
}
So subsequent reduce
calls will check to see if src
and lib
are present in the appropriate places and avoid creating them if so.
You may be asking, what about git.js
from above? We didn’t really talk about that? That’s where this chunk comes in handy:
// If we're on the final element, it's the file itself.
if (i === filePath.length - 1) {
prevDir[currDir] = 'file';
}
If we’re on the final element (indicated by the index, or i
), then we can make the assumption that we’re out of directories and need to mark the property as a file.
After all of that, we return the object
representing the file-tree!
So you see, reduce
can be used in to solve something other than math. In this case, converting an array of file-paths into a single JSON object representing the file tree, and doing it in a pretty clean way. I hope you found this example useful, and I also hope you give reduce
a try and see how awesome it is!