technical and social progress toward ECMAScript 6 at facebook


Ben Newman (Facebook)
Fluent 2014

{ github,
  twitter,
  instagram,
  facebook
}.com/benjamn

Wikipedia

titlecapitalization.com

We have the opportunity, as technologists, to make certain kinds of problems disappear forever.

“We should have been doing it this way all along!”

GitHub is strewn with better ways of doing things that never got properly evangelized.

There has to be a way forward.

Programming languages are notoriously difficult to “fix forward.”


Why isn't every Python project using Python 3 yet?

How can ECMAScript 6
avoid the Python 3 trap?


Crazy idea: ease into the new language by simulating its most useful features in the current version of JavaScript (ECMAScript 5).

Example: => function syntax


Input (ES6):

[3, 1, 10, 28].sort((a, b) => a - b)


Output (ES5):

[3, 1, 10, 28].sort(function(a, b) {
  return a - b;
}.bind(this))

First, import some utilities and parse the code:



var recast = require("recast");
var types = recast.types;
var traverse = types.traverse;
var n = types.namedTypes;
var b = types.builders;

var ast = recast.parse(
  "[3, 1, 10, 28].sort((a, b) => a - b)"
);

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {




















});
            

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {
  if (n.ArrowFunctionExpression.check(node)) {


















  }
});
            

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {
  if (n.ArrowFunctionExpression.check(node)) {
    var body = node.body;

    if (node.expression) {
      node.expression = false;
      body = b.blockStatement([b.returnStatement(body)]);
    }












  }
});
            

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {
  if (n.ArrowFunctionExpression.check(node)) {
    var body = node.body;

    if (node.expression) {
      node.expression = false;
      body = b.blockStatement([b.returnStatement(body)]);
    }

    var funExp = b.functionExpression(
      node.id, node.params, body,
      node.generator, node.expression
    );







  }
});
            

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {
  if (n.ArrowFunctionExpression.check(node)) {
    var body = node.body;

    if (node.expression) {
      node.expression = false;
      body = b.blockStatement([b.returnStatement(body)]);
    }

    var funExp = b.functionExpression(
      node.id, node.params, body,
      node.generator, node.expression
    );

    var bindExp = b.callExpression(
      b.memberExpression(funExp, b.identifier("bind"), false),
      [b.thisExpression()]
    );


  }
});
            

Next, traverse and modify the syntax tree:



traverse(ast, function(node) {
  if (n.ArrowFunctionExpression.check(node)) {
    var body = node.body;

    if (node.expression) {
      node.expression = false;
      body = b.blockStatement([b.returnStatement(body)]);
    }

    var funExp = b.functionExpression(
      node.id, node.params, body,
      node.generator, node.expression
    );

    var bindExp = b.callExpression(
      b.memberExpression(funExp, b.identifier("bind"), false),
      [b.thisExpression()]
    );

    this.replace(bindExp);
  }
});
            

Finally, reprint the code:



console.log(recast.print(ast).code);

// Which prints:
[3, 1, 10, 28].sort(function(a, b) {
  return a - b;
}.bind(this))
          


If you already have a build step for static resources, you can be cooking with arrow functions in a matter of minutes!

recast, v.


  1. to give (a metal object) a different form by melting it down and reshaping it.
  2. to form, fashion, or arrange again.
  3. to remodel or reconstruct (a literary work, document, sentence, etc.).
  4. to supply (a theater or opera work) with a new cast.

Recast recap:



var recast = require("recast");
var ast = recast.parse(source);
transform(ast); // Anything goes.
console.log(recast.print(ast).code);
          

“Non-destructive partial source transformation”

Ariya Hidayat

Example: ...rest parameters


Input (ES6):

function max(a, ...rest) {
  rest.forEach(x => { if (x > a) a = x; });
  return a;
}

Output (ES5):

function max(a) {
  var rest = Array.prototype.slice.call(arguments, 1);
  rest.forEach(function(x) { if (x > a) a = x; }.bind(this));
  return a;
}

Example: ...rest parameters



traverse(ast, function(node) {

















});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {















  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");














  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");

    var sliceArgs = [
      b.identifier("arguments"),
      b.literal(node.params.length)
    ];









  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");

    var sliceArgs = [
      b.identifier("arguments"),
      b.literal(node.params.length)
    ];

    var sliceCall = b.callExpression(sliceCallee, sliceArgs);







  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");

    var sliceArgs = [
      b.identifier("arguments"),
      b.literal(node.params.length)
    ];

    var sliceCall = b.callExpression(sliceCallee, sliceArgs);

    var restVarDecl = b.variableDeclaration("var", [
      b.variableDeclarator(node.rest, sliceCall)
    ]);



  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");

    var sliceArgs = [
      b.identifier("arguments"),
      b.literal(node.params.length)
    ];

    var sliceCall = b.callExpression(sliceCallee, sliceArgs);

    var restVarDecl = b.variableDeclaration("var", [
      b.variableDeclarator(node.rest, sliceCall)
    ]);

    node.body.body.unshift(restVarDecl);

  }
});

Example: ...rest parameters



traverse(ast, function(node) {
  if (n.Function.check(node) && node.rest) {
    var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");

    var sliceArgs = [
      b.identifier("arguments"),
      b.literal(node.params.length)
    ];

    var sliceCall = b.callExpression(sliceCallee, sliceArgs);

    var restVarDecl = b.variableDeclaration("var", [
      b.variableDeclarator(node.rest, sliceCall)
    ]);

    node.body.body.unshift(restVarDecl);
    node.rest = null;
  }
});

Example: class syntax


  Input (ES6):

class Derived extends Base {
  constructor(value) {
    super(value + 1);
  }

  getValue() {
    return super.getValue() - 1;
  }

  static getName() {
    return "Derived";
  }
}
Output (ES5):

function Derived(value) {
  Base.call(this, value + 1);
}

Derived.prototype = Object.create(Base.prototype);

Derived.prototype.constructor = Derived;

Derived.prototype.getValue = function() {
  return Base.prototype.getValue.call(this) - 1;
};

Derived.getName = function() {
  return "Derived";
};

Transform code much more involved than previous examples;
see es6-class-visitors.js for all the gory details.

What our classes used to look like:



var Class = require('Class');
var copyProperties = require('copyProperties');

function Derived(value) {
  this.parent(value + 1);
}

copyProperties(Derived.prototype, {
  getValue: function() {
    return this.parent.getValue() - 1;
  }
});

copyProperties(Derived, {
  getName: function() {
    return "Derived";
  }
});

Class.extend(Derived, Base);

Original plan:


  1. Add a jslint warning that complains when you try to commit new code that assigns .prototype properties
  2. Point to a wiki article in the warning message
  3. Post about the new syntax in some internal Facebook groups
  4. Wait for the diffs to roll in
  5. Declare victory (just in time for ECMAScript 12)

Wishful thinking!


  • jslint tells you that your code might be bad after you've written it
  • Almost no one reads wiki articles unless hopelessly stuck
  • I didn't want to make reeducating thousands of Facebook engineers my full-time job
  • As long as the old style of defining classes dominated the codebase, that's the style engineers would (reasonably!) mimic

What to do?


We have lots of this (ES5):


var Class = require('Class');
var copyProperties = require('copyProperties');

function Derived(value) {
  this.parent(value + 1);
}

copyProperties(Derived.prototype, {
  getValue: function() {
    return this.parent.getValue() - 1;
  }
});

copyProperties(Derived, {
  getName: function() {
    return "Derived";
  }
});

Class.extend(Derived, Base);
We want more of this (ES6):


class Derived extends Base {
  constructor(value) {
    super(value + 1);
  }

  getValue() {
    return super.getValue() - 1;
  }

  static getName() {
    return "Derived";
  }
}

What to do?


We want more of this (ES6):


class Derived extends Base {
  constructor(value) {
    super(value + 1);
  }

  getValue() {
    return super.getValue() - 1;
  }

  static getName() {
    return "Derived";
  }
}
So that we can ship this (ES5):


function Derived(value) {
  Base.call(this, value + 1);
}

Derived.prototype = Object.create(Base.prototype);

Derived.prototype.constructor = Derived;

Derived.prototype.getValue = function() {
  return Base.prototype.getValue.call(this) - 1;
};

Derived.getName = function() {
  return "Derived";
};

We've solved this problem before.


It's just a source tranformation! What's different?


  1. ES5 to ES6 (the “wrong” direction)
  2. Only needs to be performed once!
  3. Needs to accommodate multiple existing idioms
  4. Needs to generate pretty code, as if (re)written by hand
  5. The diffs need to be human-reviewable


“Non-destructive partial source transformation”

All told:


1647 files changed
76555 insertions(+)
78260 deletions(-)

1658 classes converted
3223 classes today

Lessons

  1. If you make the output human-readable enough, reviewers may not even realize it was machine-generated.

  1. The transform script should be absolutely littered with fail-stop assertions.

  1. Set yourself up to iterate rapidly, accommodating more and more exotic cases as you encounter them.


Meaning: you should be able to change your mind as often as you like, git reset --hard, and rerun the script.

  1. Feel free to fix rare cases by hand, but stack them in separate commits.

  1. Make the transform script idempotent. No excuses!

  1. Use GNU parallel to run the transform script in many processes simultaneously.



find ~/www/html/js/lib | \
  grep "\.js$" | \
  time parallel ~/www/scripts/bin/classify --update

228.03s user 12.25s system 1229% cpu 19.548 total
          

  1. Humans have right-of-way.

  1. Identify stakeholders, and convert whole functional units of code at once.

  1. Set intermediate milestones, and prioritize them aggressively.


Killing off Class.extend in favor of extends/super only really depended on the conversion of files using Class.extend.

  1. New code mimics existing code, and the future is longer than the past.

Thanks!

External links:


github.com/{
  facebook/jstransform,
  benjamn/ast-types,
  benjamn/recast,
  facebook/regenerator
}

code.facebook.com/projects