RSS

Simple Variadic Behavior

Info

SummaryBeginner-friendly introduction to variadic behavior.
Shared2015-10-13

Recently, I have started working on my own static site generator, react-static, to accomodate my markdown blog posting & static site needs. Another benefit is that I get to work on my Node.js and ES2015+ JavaScript skills. While I am reinventing the wheel on some levels, it is good practice.

Update: I made a library, parse-md, out of some of this behavior in order to address the need of parsing metadata from markdown files.

My latest problem to solve was how, once I had a .md (Markdown) file’s contents, to go about parsing out the blog post’s metadata (see below: the key/value pairs between the two ---s).

---
title: This is a test
description: Once upon a time, there was a test...
---

# Title of my great post
Lorem ipsum dolor...

## Some heading
Bacon ipsum...

Once I split this file based on newlines, I needed a way of finding the indices of the metadata boundary, ---, so that I could splice the array in to two pieces and be on my way. My first attempt at getting the indices looked like this:

function getMetadataIndices(lines) {
  var arr = [];
  lines.forEach((line, i) => {
    if (/^---/.test(line)) {
      arr.push(i);
    }
  });
  return arr;
}

getMetadataIndices(lines); // [0, 3]

This is a simple solution that any junior dev can do, and it accomplishes the task… but it doesn’t feel right. I am iterating over each item, testing each line and mutating an array variable when a condition is true. While it doesn’t look like much, that is a good bit going on all at once. Instinct tells me that each action could be its own simple method. I also don’t want to use a temporary variable that I mutate. However, this removes forEach from our options, as forEach returns the original array. map() to the rescue! (or so we think).

function getMetadataIndices(lines) {
  return lines.map(testForBoundary);
}

function testForBoundary(item, i) {
  if (/^---/.test(item)) {
    return i;
  }
}

getMetadataIndices(lines); // [0, undefined, undefined, 3, undefined, undefined, undefined, undefined, undefined, undefined]

Crap. Because I only return when the test is true, map doesn’t know what to return, so it returns undefined and moves on. It would be nice if we could clean out these undefineds!

Cleaning Up Our Array

How can we achieve the following desired functionality?

function getMetadataIndices(lines) {
  return lines.map(testForBoundary).clean(undefined);
}

getMetadataIndices(lines); // [0, 3]

Let’s make a function on the prototype of Array called clean:

Array.prototype.clean = function(trash) {
};

Here, we access Array’s prorotype and add our own custom method, clean and pass it one argument. Next, we need to filter out all of the undefineds in our array.

Array.prototype.clean = function(trash) {
  return this.filter(item => item !== trash);
};

But what if we need to clean more than one value out? What if we need to clean null, "" and undefined?

Variadic Behavior

In JavaScript, variadic behavior is a fancy term applied to functions that can accept and handle any number of arguments, and these are typically accessed within the function via the arguments object, which looks like an Array but is not. For example, this code will give you an error about indexOf not being defined on arguments.

Array.prototype.clean = function(trash) {
  return this.filter(item => arguments.indexOf(item) === -1);
};

Drats! arguments is very similar to an array — how can we get this to work? slice to the rescue!

Array.prototype.clean = function() {
  const args = [].slice.call(arguments);
  return this.filter(item => args.indexOf(item) === -1);
};

Without any additional arguments, slice makes a copy of an array and allows us to provide a custom receiver of array-like functionality: arguments. What is returned from the second line above is an array-ized copy of arguments. Now that args is an array of all the arguments that are passed to clean, we can pass as many options as we would like to clean out our array!

Here is more example usage of such a method:

// Usage
const arr = ["", undefined, 3, "yes", undefined, undefined, ""];
arr.clean(undefined); // ["", 3, "yes", ""];
arr.clean(undefined, ""); // [3, "yes"];

All Together

In attempting to refactor some fairly simple, though multiple-responsibility code, we end up creating a few reusable functions that will benefit us in the future, and we make our code more maintainable, testable and readable in the end. Here it is once we have finished:

function getMetadataIndices(lines) {
  return lines.map(testForBoundary).clean(undefined);
}

function testForBoundary(item, i) {
  if (/^---/.test(item)) {
    return i;
  }
}

Array.prototype.clean = function() {
  const args = [].slice.call(arguments);
  return this.filter(item => args.indexOf(item) === -1);
};

But could this be done even simpler?

p.s. Use reduce next time

You may have been wondering why we didn’t use reduce like this from the start:

lines.reduce(function(mem, item, i) {
  if (/^---/.test(item)) {
    mem.push(i);
  }
  return mem;
});

or, cleaned up a bit,

function getMetadataIndices(mem, item, i) {
  if (/^---/.test(item)) {
    mem.push(i);
  }
  return mem;
}

lines.reduce(getMetadataIndices, []);

Surprise! We totally could have, but since reduce was not our first thought when refactoring, we managed to solve our problem in another way. There are 1000 ways to solve problems, and sometimes you don’t think of the best one first, but you can still make the best with what you have at the time and refactor later.