On Github benjamn / pivotal-meetup-talk
Ben Newman (Facebook)NodeJS @ Pivotal Labs19 March 2014
{ github, twitter, instagram, facebook }.com/benjamn
Congratulations! You're not done.
Not even if you're right.
Not even if everyone agrees that you're right.
Code just left there without warranty of any kind, either expressed or implied, in the vague hope that other people face needs that are similar to the ones that motivated the author.
Often “better” just means “better tailored to my particular use case.”
In which case there's no mystery why the evangelical message went nowhere.
And it can't involve throwing away all of your code and starting over from scratch, as tempting as that might be.
As the evangelist, you have a responsibility to yourself, to your ideas, and to the people you want to help, to devise and execute an incremental migration plan.
Other people won't be willing to buy in unless you justify the effort and short-term increase in complexity.
Why isn't every Python project using Python 3 yet?
It has that 2to3 script.
The maintainers have realistic expectations.
Biggest barrier: changes in unicode semantics.
Attempting to solve a problem once and for all typically requires adapting many different, somewhat-functional past solutions.
Crazy idea: ease into the new language by simulating its most useful features in the current version of JavaScript (ECMAScript 5).
The great virtue of this idea is that it makes sense at every stage.
Even if it never ultimately connects with the next version of the language, or takes a long time getting there, we still benefit.
[3, 1, 10, 28].sort((a, b) => a - b)
[3, 1, 10, 28].sort(function(a, b) {
return a - b;
}.bind(this))
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);
Instead of simply pretty-printing the whole tree, recast.print tries to recyle the original source code wherever possible.
function max(a, ...rest) {
rest.forEach(x => { if (x > a) a = x; });
return a;
}
function max(a) {
var rest = Array.prototype.slice.call(arguments, 1);
rest.forEach(function(x) { if (x > a) a = x; }.bind(this));
return a;
}
traverse(ast, function(node) {
});
traverse(ast, function(node) {
if (n.Function.check(node) && node.rest) {
}
});
traverse(ast, function(node) {
if (n.Function.check(node) && node.rest) {
var sliceCallee = mx(mx(mx("Array", "prototype"), "slice"), "call");
}
});
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)
];
}
});
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);
}
});
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)
]);
}
});
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);
}
});
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;
}
});
class Derived extends Base {
constructor(value) {
super(value + 1);
}
getValue() {
return super.getValue() - 1;
}
static getName() {
return "Derived";
}
}
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.
Good time to mention that all our transforms are open source (click on the link to open in a new tab).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);
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);
class Derived extends Base {
constructor(value) {
super(value + 1);
}
getValue() {
return super.getValue() - 1;
}
static getName() {
return "Derived";
}
}
class Derived extends Base {
constructor(value) {
super(value + 1);
}
getValue() {
return super.getValue() - 1;
}
static getName() {
return "Derived";
}
}
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?
ES5 to ES6 (the “wrong” direction) Only needs to be performed once! Needs to accommodate multiple existing idioms Needs to generate pretty code, as if (re)written by hand The diffs need to be human-reviewableMeaning: you should be able to change your mind as often as you like, git reset --hard, and rerun the script.
So that you can keep running the script on top of those changes without having to remake the changes every time.
Fixing something unusual by hand can be much faster than teaching the transformer how to fix it, relative to the number of occurrences.
You will end up re-running the script on already-transformed files, as your script improves, no matter how confident you are.
Think about files with multiple classes, some of which you knew how to transform before you knew how to handle the others.
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
Look at that CPU percentage!
Besides speed, the main advantage here is simplicity: the transform script only needs to take one file name argument!
This assumes that individual files can be handled independently.
You can always “rebase” by re-running your script—an incredible luxury, if you think about it.
This happens so frequently that I'm tempted to say it the conversion wouldn't be possible without this property.
Even if you think you can get away with the transform without any objections, you can't avoid the complaint that the code wasn't broken before.
I didn't do this at first and it bit me.
A big long-term benefit was that I had more and more successful examples to point to over time. Social proof!
This might be the biggest lesson of all.
Killing off Class.extend in favor of extends/super only really depended on the conversion of files using Class.extend.
Convert a few high-profile internal projects Convert all internal projects Convert all proper CommonJS modules Convert non-modules that use Class.extend to modules so we can kill Class.js and start using extends/super sooner Convert relatively modern products like /messages Some day—maybe!—finish modularizing everything, but no hurry
<div>
<h3>TODO</h3>
<TodoList items={this.state.items}/>
<form onSubmit={this.handleSubmit}>
<input onChange={this.handleChange} value={this.state.text}/>
<button>{'Add #' + (this.state.items.length + 1)}</button>
</form>
</div>
React.DOM.div(null,
React.DOM.h3(null, "TODO"),
TodoList( {items:this.state.items}),
React.DOM.form( {onSubmit:this.handleSubmit},
React.DOM.input( {onChange:this.handleChange, value:this.state.text}),
React.DOM.button(null, 'Add #' + (this.state.items.length + 1))
)
)
<div>
<h3>TODO</h3>
<TodoList items={this.state.items}/>
<form onSubmit={this.handleSubmit}>
<input value={this.state.text}/>
<button>{
'Add #' + (this.state.items.length + 1)
}</button>
</form>
</div>