«

Array.reduce is pretty neat

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:

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.

Practical Example

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:

  1. Give me an array of file paths.
  2. I'm going to make an empty object that we'll fill in later.
  3. If the file isn't in any directories, I'm just going to put it on the root level of the object.
  4. If there are directories, I'm going to put them through a reduce function!:
    1. I'm going to split up the file path into an array of directories indicating hierarchy.
    2. Check each directory and make it in the appropriate place on our object.
    3. If we're on the last element, it's the actual file, so insert that as a property.
    4. Return the file-tree Object!

parseFilePath

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.

mergePathsIntoFileTree

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.

Final pieces

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!

Share Comment on Twitter