API Design &: The Bind Operator

Thanks to projects like Babel, it feels like pretty much everybody is firmly on the ES6 (and even ES7!) bandwagon. So now seems like a good time to talk about a JavaScript proposal that I think is pretty cool, and the potential impacts it’ll have on our API designs.

The bind operator has been talked about for a while and has a few different proposals, but the main idea is that you type something like this:

[1, 2, 3, [3, 2], [1]]::flatten()::unique();

and you get something that works like this:

unique.call(flatten.call([1, 2, 3, [3, 2], [1]]));

There are two things worth pointing out here:

  1. Neither flatten() nor unique() is a method of our array
  2. The code reads really well left-to-right

Contrast this with our normal functional syntax:

unique(flatten([1, 2, 3, [3, 2], [1]]));

This isn’t the worst thing, to be sure; we’re trained to recognize that the innermost functions are applied first. But, as we add more transformations, our expression becomes more unwieldy to write and to read. If it spans several lines, you may have to scan ahead to find the first operation. Oh, and are your parens balanced?

As it stands in 2015, we don’t have many options for writing code like this that reads well left-to-right. We can add methods to prototypes, add methods to a wrapper interface (a la jQuery), or compose the functions separately. Of these options, only the third is really palatable; the first has well known problems and the second only solves namespace collision issues for the authors of the wrapper, leaving the rest of us in the same position we were in when extending prototypes. With the bind operator, though, we can easily imagine a version of jQuery that doesn’t even need a wrapper:

import {find, eq, html} from 'hypothetical-jquery';
import makeBold from './makeBold';
document.getElementById('content')
::find('h3')
::eq(2)
::makeBold()
::html('new text for the third h3!');

Granted, there are still ways this could be better, but the bind operator has gained us quite a lot. Not only do we get to get to deal with real DOM elements, but we’ve eliminated namespacing issues without losing readability, explicitly declared our dependencies, and our “jQuery” library functions are on equal footing with userland functions.

As I alluded to in earlier examples, iteration utilities are another common example of APIs that would benefit from having the bind operator. Since we’ve already looked at an underscore-y example, let’s imagine a version of Immutable.js built with the bind operator in mind. How might it be different?

Well, a big chunk of the Immutable collections’ API is actually iteration methods. It’s easy to imagine those as a separate library that worked in conjunction with minimal versions of our collection data types. For example, our hypothetical Immutable.Map API might just be the members of its mutable counterpart minus the mutative methods clear(), delete(), and set().

import {List, Map} from 'hypothetical-immutable';
import {map, filter} from 'myiterlib';
const tNumbersEntries =
List.of('one', 'two', 'three', 'four', 'five')
::map((word, index) => [index + 1, word])
::filter(([index, word]) => word.substring(0, 1) === 't');
const tNumbers = new Map(tNumbersEntries);
console.log(tNumbers.keys()) // MapIterator {2, 3}
console.log(tNumbers.get(2)) // "two"
tNumbers.set(10, 'ten') // ERROR! It's immutable!

As you’ve probably noticed, there’s a common thread running through both our hypothetical jQuery and Immutable post-bind-operator APIs: when we have the ability to separate functionality from our objects without sacrificing readability, our object APIs get smaller. In other words, we’re decoupling the data from operations on the data, and getting closer to the functional ideal…right?


Up until now, we’ve been looking at usages of bind-operator-influenced APIs but haven’t looked at implementations. So what might our map(), for example, look like?

function *map(iteratee, thisArg) {
for (let v of this) {
yield iteratee.call(thisArg, v);
}
};

This is a pretty elegant example implementation, but our use of this on the second line jumps out immediately. Ironically, while the bind operator has allowed us to decouple operations from data structures, it’s also kept us bound to the OOP paradigm of methods acting on a single, special (and in JavaScript’s case, implicit and magic) argument. A “normal” functional implementation would likely accept the iterable as the first argument instead:

function *map(collection, iteratee, thisArg) {
for (let v of collection) {
yield iteratee.call(thisArg, v);
}
};

This version wouldn’t be useful with the bind operator and therefore wouldn’t do anything to give us that left-to-right application order we were looking for. Of course, it’s trivial to write a higher-order function to convert it into something that operates on this, but it’s still hard to shake the feeling that there might be a better way — one that would satisfy our goals of decoupling and left-to-right application without having to design APIs as detached methods. Maybe we could coneptualize binding as a special case of partial application and take inspiration from something like LiveScript’s approach?

List.of('one', 'two', 'three', 'four', 'five')
|> map _, (word, index) -> [index + 1, word]
|> filter _, ([index, word]) -> word.substring(0, 1) === 't'

In the end, I’m still excited about the bind operator but I have to admit that considering its API design implications has tempered my excitement some. In the best case, maybe we wind up with a wrapper utility like promisify for converting normal functional APIs to unbound methods. In the worst: a proliferation of libraries differentiated only by whether they operate on normal arguments or this (along with, of course, enough small API differences to be confusing when jumping between the two).

You can try out the bind operator now with the esdown repl or discuss the proposals on esdiscuss.