JavaScript, make me a Triple Function Sandwich

JavaScript December 5, 2016

This post first appeared on the Big Nerd Ranch blog.

You probably knew that despite the name and superficially similar syntax, JavaScript is unrelated to Java. The unfortunate name “JavaScript” originated when the company responsible for creating JavaScript—Netscape Communications—entered into a license agreement with Sun in 1995. Thus, many of the design patterns you might know from Java, Ruby or other class-oriented programming languages are not idiomatic to JavaScript.

So what design patterns are idiomatic to JavaScript?

JavaScript’s object-oriented behavior imitates Self (a dialect of Smalltalk), but the overall programming paradigm is heavily influenced by its functional programming heritage. Moreover, JavaScript has some unique functional patterns of its own hiding in plain sight throughout popular libraries and Web APIs.

Let’s dissect two in-the-wild patterns from the JavaScript ecosystem—we’ll call them the Function Factory Function and Triple Function Sandwich.

Function Factory Function

The Function Factory Function is a function that follows the Factory method pattern, but returns a new function. Most Factories return objects, but thanks to first-class functions in JavaScript, it’s common for the Factory to build a function. In functional terminology, FFFs are often an example of a Higher-order Function.

If you’ve used the Array.prototype.sort function, you probably used a higher-order function to generate another function that can sort a list of objects by a particular property:

var Sorter = extract => {
  return (a, b) => {
    var av = extract(a),
        bv = extract(b);

    return av < bv ? -1 : (av > bv ? 1 : 0);
  };
};

var people = [
  { name: 'Alex', age: 36 },
  { name: 'Beth', age: 30 },
  { name: 'Chris', age: 27 }
];

var sortByAge = Sorter(p => p.age);

people.sort(sortByAge).map(p => p.name);
// => ["Chris", "Beth", "Alex"]

The Function Factory Function follows a similar structure, but unlike a higher-order function, it doesn't require a function as an argument. Here's an example of an FFF used to generate middleware in Koa (a Node.js web framework):

var Koa = require('koa');
var compress = require('koa-compress');
var serve = require('koa-static');

var app = new Koa();

app.use(compress());
app.use(serve('./app'));

If Koa was more OOPsy, calling compress() and serve() would probably generate objects, but in functional programming we can capture local variables as state and return a function with access to those variables. This way, we are still applying the principle of Encapsulation, but without objects!

How would we use the Function Factory Function pattern in our own code? Suppose we are building a Single Page App (SPA) for the next version of Google Docs, and we want to prevent the user from navigating to another document if there are unsaved changes. If the router fired a beforetransition event, it would be nice if we could “lock” the page and make sure the user really wants to navigate away before allowing the transition. We could write a lock() Function Factory Function to tidy this up; here’s how we might use it:

var unlock = lock(ask => {
  if (checkUnsavedChanges() &&
      ask('You will lose changes!')) {
    discardEdits();
  }
});

// ...once this page is no longer around
// and we need to clean up after ourselves:
unlock();

The lock() function generates a new function called unlock() that can be invoked to stop watching for page transitions. This will be useful if the user navigates away from this document and this page needs to be deactivated.

Using lock() can tidy things up nicely: if the user attempts to navigate away from the document, we can check if there are any edits, and if there are we can ask() the user if they are okay with losing changes. If they are, we can discard those edits and move on.

We could implement the lock() function like this:

var $window = $(window);
var lock = cb => {
  var handler = event => {
    var abort = () => {
      event.preventDefault();
    };

    var ask = message => {
      var okay = window.confirm(message);
      if (!okay) { abort(); }
      return okay;
    };

    cb(ask);
  };

  $window.on('beforetransition', handler);
  return () => {
    $window.off('beforetransition', handler);
  };
}

Whenever the user attempts to transition away from the document, we execute the callback and pass in a helper function called ask() to prompt the user. If the user cancels, we .preventDefault() on the event to cancel the transition.

It’s a nice micro API that can tidy up gnarly code elsewhere! This pattern is an elegant alternative to a class-oriented approach where we would attach state and the unlock method to an object. Incidentally, the lock() function is also an example of the next design pattern: the Triple Function Sandwich.

Triple Function Sandwich

Used Promises lately? You’re writing a Triple Function Sandwich!

var promise = new Promise(function(resolve, reject) {
  setTimeout(resolve, 1000);
});

promise.then(() => {
  console.log("It's been a second.");
});

Take a look at all the nested functions: we are invoking the Promise() function by passing it a function that will be invoked and passed yet another function resolve() as an argument. Usually you see this kind of code when a callback needs to be executed asynchronously, but that's not the case for the Promise() function—it will immediately run the given callback:

console.log('1');
var promise = new Promise(function(resolve, reject) {
  console.log('2');
});
console.log('3');

// => 1
// => 2
// => 3

So if the callback isn’t being run asynchronously, why the sandwich? Function sandwiches are a form of cooperative programming: they allow one function to cede control to another function (your callback), but provide a public API for modifying the calling function’s behavior.

We can use this pattern ourselves to create an async-friendly for-loop! Suppose we want to iterate over a list of numbers and print each one-by-one after waiting for a few seconds. Standard loops in JavaScript run as fast as they can, so to wait between iterations we will need to write our own iterate() function. Here’s how we would use it:

var list = [1,2,3,4,5,6,7,8,9];
var promise = iterate(list, (curr, next, quit) => {
  console.log(curr);
  if (curr < 3) {
    setTimeout(next, curr * 1000);
  } else {
    quit();
  }
});

promise.then(finished => {
  if (finished) {
    console.log('All done!');
  } else {
    console.log('Done, but exited early.');
  }
});

// => 1
// => 2
// => 3
// => Done, but exited early.

This example will immediately print 1, then 1 second later it will print 2, 2 seconds later it will print 3 and quit() the loop, and 'Done, but exited early. will be printed. Our callback function receives three arguments to control the loop: curr which contains the current element of the list, next() which advances to the next iteration of the loop, and quit() which exits the loop prematurely.

The iterate() function itself returns a Promise that will resolve once it finishes iterating over the list. This Promise will resolve to true if the loop finished iterating over all the elements, or false if the quit() function was invoked to exit the loop early. Notice the Triple Function Sandwich is not as obvious: the sandwich starts with iterate(), the second argument is a function, and the second parameter of that function, next(), is also a function.

Despite this complex behavior, iterate() only takes a few lines of code to implement!

var iterate = (list, cb) => {
  return new Promise(resolve => {
    var counter = 0;
    var length = list.length;

    var quit = () => {
      resolve(false);
    }

    var next = () => {
      if (counter < length) {
        cb(list[counter++], next, quit);
      } else {
        resolve(true);
      }
    }

    next();
  });
};

iterate() initializes a counter variable, defines a few functions, then kicks off iteration by calling next(). Every time next() is invoked, it executes cb() and passes in the current element, next() itself, and the quit() function. If it has finished iterating, it resolves the overall Promise to true.

If we had written this same code in a more OOPsy style, it might look like:

var Iterator = function(list, cb) {
  this.list = list;
  this.cb = cb;
  this.counter = 0;
  this.length = list.length;
  this.promise = new Promise(
    resolve => { this.resolve = resolve; }
  );
};
Iterator.prototype.quit = function() {
  this.resolve(false);
};
Iterator.prototype.next = function() {
  if (this.counter < this.length) {
    this.cb(this.list[this.counter++]);
  } else {
    this.resolve(true);
  }
};
Iterator.prototype.start = Iterator.prototype.next;

var list = [1,2,3,4,5,6,7,8,9];
var iterator = new Iterator(list, (curr) => {
  console.log(curr);
  if (curr < 3) {
    setTimeout(() => iterator.next(), curr * 1000);
  } else {
    iterator.quit();
  }
});
iterator.start();

iterator.promise.then(finished => {
  if (finished) {
    console.log('All done!');
  } else {
    console.log('Done, but exited early.');
  }
});

Looks a little clumsy in comparison. Both versions solve the same problem with a form of cooperative programming: the former by encoding state in local variables and “pushing” in a public API to the callback, and the latter by creating a special object with state and methods. Interestingly, this example shows that Encapsulation is not just an OOP principle—the functional approach also hides its state (local variables) and provides a public API for modifying that state.

The Triple Function Sandwich is not just for async programming! If you find yourself resorting to an object-oriented approach when you need to break down a function into several steps while preserving state, you might just try a bite of the Triple Function Sandwich. Both approaches provide encapsulation and solve cooperative programming problems, but the functional approach is a thing of beauty that does credit to JavaScript’s hidden elegance.

Jonathan Lee Martin

Jonathan is an educator, writer and international speaker. He guides developers — from career switchers to senior developers at Fortune 100 companies — through their journey into web development.