easing into ECMAScript 6 and beyond


Ben Newman (Facebook)
EmpireJS 2014

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

Wikipedia

titlecapitalization.com

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

Example: that one weird ~ trick


Input:

if (["a", 2, true].indexOf(x) != -1) {
  console.log(x + ' was either "a", 2, or true');
}
            

Output:

            

First, import some utilities and parse the code:


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

var ast = recast.parse(
  '["a", 2, true].indexOf(x) != -1'
);

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({






















});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {













  }







});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;












  }







});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {










    }
  }







});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      }





    }
  },

  isNegativeOne: function(node) {




  }
});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      } else return;





    }
  },

  isNegativeOne: function(node) {




  }
});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      } else return;
      node = b.unaryExpression("~", node);




    }
  },

  isNegativeOne: function(node) {




  }
});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      } else return;
      node = b.unaryExpression("~", node);
      if (operator.charAt(0) === "=") {
        node = b.unaryExpression("!", node);
      }

    }
  },

  isNegativeOne: function(node) {




  }
});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      } else return;
      node = b.unaryExpression("~", node);
      if (operator.charAt(0) === "=") {
        node = b.unaryExpression("!", node);
      }
      return this.genericVisit(node);
    }
  },

  isNegativeOne: function(node) {




  }
});
            

Next, traverse and modify the syntax tree:


var NotEqualNegativeOneVisitor = Visitor.extend({
  visitBinaryExpression: function(node) {
    var operator = node.operator;
    if (/(!=|==)=?/.test(operator)) {
      if (this.isNegativeOne(node.left)) {
        node = node.right;
      } else if (this.isNegativeOne(node.right)) {
        node = node.left;
      } else return;
      node = b.unaryExpression("~", node);
      if (operator.charAt(0) === "=") {
        node = b.unaryExpression("!", node);
      }
      return this.genericVisit(node);
    }
  },

  isNegativeOne: function(node) {
    return n.UnaryExpression.check(node) &&
      node.operator === "-" &&
      n.Literal.check(node.argument) &&
      node.argument.value === 1;
  }
});
            

Finally, visit the AST and reprint the code:


var visitor = new NotEqualNegativeOneVisitor();
ast = visitor.visit(ast);
console.log(recast.print(ast).code);

// Which prints:
~["a", 2, true].indexOf(x)
            

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

Moral hazard:


  • be the laziest, most productive programmer at Facebook
  • be the the change I want to see in the world

[Agent Smith voice]


One of these lives, Mr. Newman, has a future.

The other... also has a future.

Confession:


I gave into temptation for a time.

All told:


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

1658 classes converted
3582 classes today
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
          


In case you're curious about the details, I gave a whole talk about this project last month (slides, video).

I hope these numbers demonstrate why the whole meme of programmer efficiency multiples (10x, 100x...) is total nonsense.

I hope these numbers demonstrate why the whole meme of programmer efficiency multiples (10x, 100x...) is total nonsense.

But I also hope they demonstrate the power of the right tool to make a difficult problem disappear completely.

“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).

“Linguistic time travel”

Example: => function syntax


Input (ES6):

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


Output (ES5):

            

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!

What about something trickier?


function *fibonacci(limit) {
  var a = 0;
  var b = 1;

  limit = limit || Infinity;

  while (a <= limit) {
    yield a;

    var next = a + b;
    a = b;
    b = next;
  }
}
            
var g = fibonacci(10);

console.log(g.next().value); // 0
console.log(g.next().value); // 1
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 5
console.log(g.next().value); // 8
console.log(g.next().done); // true
            
How hard could it be to translate this function into one that no longer contains function* or yield?

It's the trickiest code I've ever written.

function *fibonacci(limit) {
  var a = 0;
  var b = 1;

  limit = limit || Infinity;

  while (a <= limit) {
    yield a;

    var next = a + b;
    a = b;
    b = next;
  }














}
            
function *fibonacci(limit) {
  var a, b, next;

  a = 0;
  b = 1;

  limit = limit || Infinity;

  while (a <= limit) {
    yield a;

    next = a + b;
    a = b;
    b = next;
  }












}
            
function fibonacci(limit) {
  var a, b, next;

  return function*() {
    a = 0;
    b = 1;

    limit = limit || Infinity;

    while (a <= limit) {
      yield a;

      next = a + b;
      a = b;
      b = next;
    }
  };










}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {




















    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
















    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }











    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }








    case 11:
    case "end":
      return context.stop();
    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }
      context.next = 6;
      return a;






    case 11:
    case "end":
      return context.stop();
    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }
      context.next = 6;
      return a;
    case 6:
      next = a + b;
      a = b;
      b = next;


    case 11:
    case "end":
      return context.stop();
    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }
      context.next = 6;
      return a;
    case 6:
      next = a + b;
      a = b;
      b = next;
      context.next = 3;
      break;
    case 11:
    case "end":
      return context.stop();
    }
  };
}
            
function fibonacci(limit) {
  var a, b, next;

  return wrapGenerator(function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }
      context.next = 6;
      return a;
    case 6:
      next = a + b;
      a = b;
      b = next;
      context.next = 3;
      break;
    case 11:
    case "end":
      return context.stop();
    }
  }, fibonacci, this);
}
            
var fibonacci = function fibonacci(limit) {
  var a, b, next;

  return wrapGenerator(function(context) {
    while (1) switch (context.next) {
    case 0:
      a = 0;
      b = 1;
      limit = limit || Infinity;
    case 3:
      if (!(a <= limit)) {
        context.next = 11;
        break;
      }
      context.next = 6;
      return a;
    case 6:
      next = a + b;
      a = b;
      b = next;
      context.next = 3;
      break;
    case 11:
    case "end":
      return context.stop();
    }
  }, fibonacci, this);
};
            

The next step: async functions


async function chainAnimations(elem, animations) {
  var ret = null;
  for (var i = 0; i < animations.length; ++i)
    ret = await animations[i](elem);
  return ret;
}

chainAnimations($("#card"), [
  flip(100), slide(200), ...
]).then(function() {
  console.log("all done!");
});
          
function chainAnimations(elem, animations) {
  var ret, i;
  return wrapGenerator.async(function(context) {
    while (1) switch (context.next) {
    case 0:
      ret = null;
      i = 0;
    case 2:
      if (!(i < animations.length)) {
        context.next = 9;
        break;
      }
      context.next = 5;
      return animations[i](elem);
    case 5:
      ret = context.sent;
    case 6:
      ++i;
      context.next = 2;
      break;
    case 9:
      return context.abrupt("return", ret);
    case 10:
    case "end":
      return context.stop();
    }
  }, null, this);
}
          
wrapGenerator.async = function(innerFn, self, tryList) {
  return new Promise(function(resolve, reject) {
    var generator = wrapGenerator(innerFn, self, tryList);
    var callNext = step.bind(generator.next);
    var callThrow = step.bind(generator.throw);

    function step(arg) {
      try {
        var info = this(arg);
        var value = info.value;
      } catch (error) {
        return reject(error);
      }

      if (info.done) {
        resolve(value);
      } else {
        Promise.resolve(value).then(callNext, callThrow);
      }
    }

    callNext();
  });
};
          

Support for async functions and the await keyword has already been fully implemented in this pull request.

But wait!


When can we expect native support for async functions and the await keyword?

[Dorothy voice]


Toto, I have a feeling we're not talking about ECMAScript 6 anymore...

Always Be Transpiling


Incremental transpilation is the key to avoiding the Python 3 trap.


It's how we bring about the future without choking on it.


It's how we know, sooner rather than later, how great the future will be.


It is, quite literally, linguistic time travel.

But it's still really hard!


Not everything can be transpiled.


AST transforms don't always play well together.


Niceties like source maps become an absolute necessity when debugging generated code.


The language specification can change out from under you.

The JavaScript Infrastructure team at Facebook will be working on this problem for as long as it takes to make the process seamless.

It's all going to be open-source.


We're only just getting started.


You are more than welcome to help.

Thanks!

External links:


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

code.facebook.com/projects