Ben Newman
(Facebook)
NodeJS
@ Pivotal Labs
19 March 2014
{ github, twitter, instagram, facebook }.com/benjamn
Why isn't every Python project using Python 3 yet?
Crazy idea: ease into the new language by simulating its most useful features in the current version of JavaScript (ECMAScript 5).
=>
function syntax[3, 1, 10, 28].sort((a, b) => a - b)
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)" );
traverse(ast, function(node) { });
traverse(ast, function(node) { if (n.ArrowFunctionExpression.check(node)) { } });
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)]); } } });
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 ); } });
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()] ); } });
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); } });
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!
var recast = require("recast"); var ast = recast.parse(source); transform(ast); // Anything goes. console.log(recast.print(ast).code);
...rest
parametersfunction max(a, ...rest) { rest.forEach(x => { if (x > a) a = x; }); return a; }
...rest
parameterstraverse(ast, function(node) { });
...rest
parameterstraverse(ast, function(node) { if (n.Function.check(node) && node.rest) { } });
...rest
parameterstraverse(ast, function(node) { if (n.Function.check(node) && node.rest) { var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call"); } });
...rest
parameterstraverse(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) ]; } });
...rest
parameterstraverse(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); } });
...rest
parameterstraverse(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) ]); } });
...rest
parameterstraverse(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); } });
...rest
parameterstraverse(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; } });
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.
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);
jslint
warning that complains when you
try to commit new code that assigns
.prototype
properties
jslint
tells you that your code might
be bad after you've written it
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"; } } |
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"; }; |
It's just a source tranformation! What's different?
Meaning: you should be able to change your mind as
often as you like, git reset --hard
, and rerun
the script.
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
Killing off Class.extend
in favor
of extends
/super
only really
depended on the conversion of files
using Class.extend
.
new
keywords
or not, how to handle children, what to do about
whitespace, React.DOM
namespacing, &c.
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
function*
or yield
?
I've been jokingly referring to this side project as "my life's
work" for so long that I'm terrified it might
actually be finished soon.
— Ben Newman (@benjamn)
May 8, 2013
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(); } }, this); }
context
do?Context.prototype.stop = function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; };
Before returning, the runtime must check to see if an unhandled exception still needs to be thrown.
for
-in
loopsContext.prototype.keys = function(object) { };
(runtime)
for
-in
loopsContext.prototype.keys = function(object) { var keys = []; for (var key in object) keys.push(key); keys.reverse(); };
(runtime)
for
-in
loopsContext.prototype.keys = function(object) { var keys = []; for (var key in object) keys.push(key); keys.reverse(); return function next() { }; };
(runtime)
for
-in
loopsContext.prototype.keys = function(object) { var keys = []; for (var key in object) keys.push(key); keys.reverse(); return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } }; };
(runtime)
for
-in
loopsContext.prototype.keys = function(object) { var keys = []; for (var key in object) keys.push(key); keys.reverse(); return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } next.done = true; return next; }; };
(runtime)
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } case 9: case "end": return context.stop(); } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } k = context.t1.value; case 9: case "end": return context.stop(); } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } k = context.t1.value; context.next = 6; return callback(k, obj[k]); case 9: case "end": return context.stop(); } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } k = context.t1.value; context.next = 6; return callback(k, obj[k]); case 6: context.next = 2; break; case 9: case "end": return context.stop(); } }, this); }
for
-in
loopsfunction *objMap(obj, callback) { before(); for (var k in obj) { yield callback(k, obj[k]); } after(); }
function objMap(obj, callback) { var k; return wrapGenerator(function(context) { while (1) switch (context.next) { case 0: before(); context.t0 = context.keys(obj); case 2: if ((context.t1 = context.t0()).done) { context.next = 8; break; } k = context.t1.value; context.next = 6; return callback(k, obj[k]); case 6: context.next = 2; break; case 8: after(); case 9: case "end": return context.stop(); } }, this); }
Check out facebook.github.io/regenerator for a convenient way to experiment with insane code and easily report any bugs that you find.
I can go into a lot more detail if there are questions, and of course https://github.com/facebook/regenerator is the final authority on how the transformation currently works.
Right now, though, I want to talk about something
much less technical.
How can you stay motivated to finish a difficult side project that isn't directly related to your day job?
Writing tests is even more important for side projects than it is for day-to-day coding.
Time your breaks for when you're pretty sure you know what you need to do next.
Guard your secrets, and don't oversell an unfinished project, but let other people in when you have something to share.
Even when they seem to have a head start.
You have a distinct advantage as the underdog, because you know exactly what success would look like.
Whenever you feel discouraged, realize that other people will probably be discouraged for the same reason.
Day job projects have to be mostly correct when you ship, but that's fortunately not the case for side projects.
Ship as soon as you're confident you can fix new problems quickly.
It makes people feel great to report real problems, especially if they're straightforward to fix.
Preemptively file known bugs and don't worry about fixing all of them before you ship.
"Report a bug" link next to the live editor.
Restrict the scope of your project to encourage interoperability.
As long as tests are passing, merge first and tweak later.
It might just be that your enthusiasm is infectious,
but that counts for a lot.
Even technical friends have an emotional side that you will need to appeal to.
And let's be honest: you have the same emotional needs, and you are the primary audience for the story you're telling.
github.com/{ benjamn/pivotal-meetup-talk, facebook/jstransform, benjamn/recast, benjamn/ast-types, facebook/regenerator } code.facebook.com/projects