Skip to content

Latest commit

 

History

History
143 lines (97 loc) · 6.74 KB

end_of_days_ellipses.md

File metadata and controls

143 lines (97 loc) · 6.74 KB

The End of Days: Implementing a CoffeeScript Feature in Pure JavaScript

This kind of thing normally wouldn't merit a full blog post all by itself, but just in case today really is the end of the world, I'm taking no chances and sharing the idea while I can.

The CoffeeScript programming language has a useful feature: If a parameter of a method is written with trailing ellipses, it collects a list of parameters into an array. It can be used in various ways, and the CoffeeScript transpiler does some pattern matching to sort things out, but 80% of the use is to collect a variable number of arguments without using the arguments pseudo-variable, and 19% of the uses are to collect a trailing list of arguments.

Here's what it looks like collecting a variable number of arguments and trailing arguments:

leftPartial = (fn, args...) ->
  (remainingArgs...) ->
    fn.apply(this, args.concat(remainingArgs))

Which translates to:

var leftPartial,
  __slice = [].slice;

leftPartial = function() {
  var args, fn;
  fn = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
  return function() {
    var remainingArgs;
    remainingArgs = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
    return fn.apply(this, args.concat(remainingArgs));
  };
};

These are very handy features. Here's our bogus, made-up attempt to write our own mapper function:

mapper = (fn, elements...) ->
  elements.map(fn)

mapper ((x) -> x*x), 1, 2, 3
  #=> [1, 4, 9]

squarer = leftPartial mapper, (x) -> x*x

squarer 1, 2, 3
  #=> [1, 4, 9]

It's true we can always manipulate the arguments thingummy variable by hand every time we want to collect a variable number of arguments into an array, but what if we don't want to constantly inject boilerplate noise into what ought to be simple functions? What if we are concerned that we'll end up making some fencepost error at four in the morning when fixing a critical bug?

It's good to have tools do this for us. So, should we switch to CoffeeScript?

hell freezes over

It must be the end of days. Or Hell has frozen over, because although I like CoffeeScript and have even written a book about it, I am not going to say, "switch to CoffeeScript." Nor am I going to say, "Nonsense, args = 2 <= arguments.length ? __slice.call(arguments, 1) : [] is perfectly readable, kids get off my lawn."

What I say is, let's use JavaScript to implement this feature for us.

Here's how we want to write our functions in JavaScript so that they're a lot more like CoffeeScript:

var leftPartial = function (fn, args...) {
  return function (remainingArgs...) {
    return fn.apply(this, args.concat(remainingArgs));
  };
};

The missing piece is that JavaScript doesn't support ellipses, those trailing periods CoffeeScript uses to collect arguments into an array. JavaScript is a functional language, so let's write a function that collects trailing arguments into an array for us:

var __slice = [].slice;  
  
function ellipses (fn) {
  if (fn.length < 1) return fn;
  
  return function () {
    var args = 1 <= arguments.length ? __slice.call(arguments, 0, fn.length - 1) : [];
    
    args.push(fn.length <= arguments.length ? __slice.call(arguments, fn.length - 1) : []);
    return fn.apply(this, args);
  }
}

And now, we have a function that adds ellipses to a function. Here's what we write:

var leftPartial = ellipses( function (fn, args) {
  return ellipses( function (remainingArgs) {
    return fn.apply(this, args.concat(remainingArgs))
  })
})

// Let's try it!

var mapper = ellipses( function (fn, elements) {
  return elements.map(fn)
});

mapper(function (x) { return x * x }, 1, 2, 3)
  //=> [1, 4, 9]

var squarer = leftPartial(mapper, function (x) { return x * x });

squarer(1, 2, 3)
  //=> [1, 4, 9]

Works like a charm! So what have we seen?

  1. CoffeeScript has a nice feature.
  2. That nice feature can be emulated in JavaScript using JavaScript's existing strength: Programming with first-class functions.
  3. When people suggest that you have to choose between JavaScript an expressive code, they are offering a false dichotomy.
  4. If today isn't the end of the world, it may instead be the day hell froze over.

Fine print: Of course, ellipses introduces an extra function call and may not be the best choice in a highly performance-critical piece of code. Then again, using arguments is considerably slower than directly accessing argument bindings, so if the performance is that critical, maybe you shouldn't be using a variable number of arguments in that section.

You be the judge.

p.s. "Ellipses" and "leftPartial" can be found in the book JavaScript Allongé, a book focused on working with functions in JavaScript, including combinators, constructors, methods, and decorators. You can download a free sample PDF.

Feedback welcome. Discuss on hacker news or reddit.


My recent work:

JavaScript AllongeCoffeeScript RistrettoKestrels, Quirky Birds, and Hopeless Egocentricity


Reg Braithwaite | @raganwald