Ben Newman
(Meteor)
11 May 2015
{ github,
twitter,
instagram,
facebook
}.com/benjamn
benjamn.github.io/goto2015-talk
// ES6 Arrow function
[1, 3, 21, 10].sort((a, b) => a - b)
// ES5 Function expression
[1, 3, 21, 10].sort(function (a, b) {
return a - b;
})
A “reasonable” translation must produce code that is readable, debuggable, and recognizable as JavaScript, rather than (say) generating bytecode that runs on a VM implemented in JavaScript.
JavaScript has a strict run-to-completion execution model, meaning that the current call stack must unwind completely before any other events can be handled by the event loop.
If you've ever wished you could simply pause the
current call stack, let some other
events run, and then later resume where
you left off,
new Fiber(function () {
console.log("before");
sleep(1000);
console.log("after");
}).run();
function sleep(ms) {
var fiber = Fiber.current;
setTimeout(fiber.run, ms);
Fiber.yield();
}
new Fiber(function () {
console.log("before");
sleep(1000); // Seemingly synchronous!
console.log("after");
}).run();
function sleep(ms) {
var fiber = Fiber.current;
setTimeout(fiber.run, ms);
Fiber.yield();
}
Aikido, AngelScript, BCPL, Pascal, BETA, BLISS, C#, ChucK, D, Dynamic C, Erlang, F#, Factor, GameMonkey Script, Go, Haskell, High Level Assembly, Icon, Io, Julia, Limbo, Lua, Lucid, µC++, MiniD, Modula-2, Nemerle, Perl, PHP, Picolisp, Prolog, Python, Ruby, Rust, Sather, Scheme, Self, Simula 67, Squirrel, Stackless Python, SuperCollider, Tcl, urbiscript
Aikido, AngelScript, BCPL, Pascal, BETA, BLISS, C#, ChucK, D, Dynamic C, Erlang, F#, Factor, GameMonkey Script, Go, Haskell, High Level Assembly, Icon, Io, Julia, Limbo, Lua, Lucid, µC++, MiniD, Modula-2, Nemerle, Perl, PHP, Picolisp, Prolog, Python, Ruby, Rust, Sather, Scheme, Self, Simula 67, Squirrel, Stackless Python, SuperCollider, Tcl, urbiscript
async and
await
async function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
getOrCreateUser("ben").then(function (ben) {
console.log(ben.name);
});
Planned for ES7, available today via Regenerator (ES3+) or es7-async-await (ES6+).
async function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
getOrCreateUser("ben").then(function (ben) {
console.log(ben.name);
});
Slightly more boilerplate, though imagine
if await
appeared inside a loop!
function createUser(name) {
return users.insert({ name: name }).then(function (response) {
return users.findOne(response._id);
});
}
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
getOrCreateUser("ben").then(function (ben) {
console.log(ben.name);
});
Slightly more boilerplate, though imagine
if await
appeared inside a loop!
function createUser(name) {
return users.insert({ name: name }).then(function (response) {
return users.findOne(response._id);
});
}
function getOrCreateUser(name) {
return users.findOne({ name: name }).then(function (user) {
return user || createUser(name);
});
}
getOrCreateUser("ben").then(function (ben) {
console.log(ben.name);
});
Slightly more boilerplate, though imagine
if await
appeared inside a loop!
function createUser(name) {
return users.insert({ name: name }).then(function (response) {
return users.findOne(response._id);
});
}
function getOrCreateUser(name) {
return users.findOne({ name: name }).then(function (user) {
return user || createUser(name);
});
}
function createUser(name, callback) {
users.insert({ name: name }).then(function (response) {
users.findOne(response._id).then(function (user) {
callback(null, user);
}, function (error) {
callback(error);
});
}, function (error) {
callback(error);
});
}
function getOrCreateUser(name) {
return users.findOne({ name: name }).then(function (user) {
return user || createUser(name);
});
}
function createUser(name, callback) {
users.insert({ name: name }).then(function (response) {
users.findOne(response._id).then(function (user) {
callback(null, user);
}, function (error) {
callback(error);
});
}, function (error) {
callback(error);
});
}
function getOrCreateUser(name, callback) {
users.findOne({ name: name }).then(function (user) {
if (user) callback(null, user);
else createUser(name, callback);
}, function (error) {
callback(error);
});
}
async
and await without any new
syntax.
async function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
});
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await users.findOne(response._id);
});
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
});
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
});
let getOrCreateUser = async(function (name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
});
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
});
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || await createUser(name);
});
Just function calls!
async
and await without any new
syntax.
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
});
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || await(createUser(name));
});
Just function calls!
function await(argument) {
}
function await(argument) {
var fiber = Fiber.current;
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
return Fiber.yield();
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
Promise.resolve(argument)
return Fiber.yield();
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
Promise.resolve(argument).then(function (result) {
}, function (error) {
});
return Fiber.yield();
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
Promise.resolve(argument).then(function (result) {
fiber.run(result);
}, function (error) {
});
return Fiber.yield();
}
function await(argument) {
var fiber = Fiber.current;
assert.ok(
fiber instanceof Fiber,
"Cannot await without a Fiber"
);
Promise.resolve(argument).then(function (result) {
fiber.run(result);
}, function (error) {
fiber.throwInto(error);
});
return Fiber.yield();
}
function async(fn) {
}
function async(fn) {
return function () {
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
})
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
fn.apply(self, args)
})
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
resolve(fn.apply(self, args));
})
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
try {
resolve(fn.apply(self, args));
} catch (error) {
}
})
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
try {
resolve(fn.apply(self, args));
} catch (error) {
reject(error);
}
})
});
};
}
function async(fn) {
return function () {
var self = this;
var args = arguments;
return new Promise(function (resolve, reject) {
new Fiber(function () {
try {
resolve(fn.apply(self, args));
} catch (error) {
reject(error);
}
}).run();
});
};
}
Fibers!
let createUser = async(function (name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
});
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || await(createUser(name));
});
How many of these function calls are really necessary? Let's try removing some...
function createUser(name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
}
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || await(createUser(name));
});
Those await calls
in createUser are legal as
long as some async function
is on the call stack.
function createUser(name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
}
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || createUser(name);
});
Since createUser no longer
returns a Promise,
getOrCreateUser no longer
needs to await it.
function createUser(name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
}
let getOrCreateUser = async(function (name) {
let user = await(users.findOne({ name: name }));
return user || createUser(name);
});
In fact, if you're willing to adopt the practice of running
all your code in a Fiber...
function createUser(name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
}
function getOrCreateUser(name) {
let user = await(users.findOne({ name: name }));
return user || createUser(name);
}
... you don't even necessarily have to
wrap getOrCreateUser as
an async function!
function createUser(name) {
let response = await(users.insert({ name: name }));
return await(users.findOne(response._id));
}
function getOrCreateUser(name) {
let user = await(users.findOne({ name: name }));
return user || createUser(name);
}
In this coding style,
the await function becomes a
tool for evaluating promises “synchronously.”
async, so that they can run
in parallel.
let [ben, marley] = await Promise.all([
getOrCreateUser("ben"),
getOrCreateUser("marley")
]);
If getOrCreateUser
is async, then it returns
a Promise, so
marley need not wait for
ben.
Minimongo
is essentially a sophisticated cache that supports a subset
of the MongoCollection API
(.find,
.findOne,
.insert,
.update,
.remove).
These operations have to return immediately, and sometimes no results are available the first time!
Acceptable because Meteor automatically rerenders the UI whenever different data become available.
If we wanted the database access API on the client to work
like the one on the server, we could
rewrite both to
return Promise objects.
Now that Promises are baked
into ES6, most new asynchronous APIs should be written in that
style.
You could then think of await
merely as a convenience that happens to be available on the
server.
Coroutines are a relaxation of
ES7 async
and await
syntax, in which
await is allowed to appear in
the body of any function called within
a Fiber,
instead of being restricted to the bodies of functions that
are explicitly marked
async.
async function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
async function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || await createUser(name);
}
function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || createUser(name);
}
function createUser(name) {
let response = await users.insert({ name: name });
return await users.findOne(response._id);
}
function getOrCreateUser(name) {
let user = await users.findOne({ name: name });
return user || createUser(name);
}
This future is possible.
This is great news for a framework like
Meteor, because we can wrap our
top-level request and event handlers
in Fibers,
then use await to implement
certain asynchronous library
operations, and none of the code in
between has to know about coroutines at
all.
Best of all, the adoption of
ES7 async
and await means TC39 will not
have to invent any new syntax if we ever decide to adopt
coroutines, because anything you might
want to do with coroutines can be expressed in terms
of async
and await,
just with slightly relaxed rules about
where await can legally
appear.
Fibers in the
browser is difficult.
For a framework that strives to be isomorphic, this inconsistency is unfortunate.
Fibers
using ES7 async and
await?
Technically yes, though it would
require awaiting any
expression that might possibly evaluate to
a Promise.
Any function containing
an await expression would have
to become an async function.
The results of all those
new async functions would have
to be awaited, and so on.
Without strong type inference, nearly every function in your
codebase would become async.
async function createUser(name) {
var response = await users.insert({ name: name });
return await users.findOne(response._id);
} |
async function getOrCreateUser(name) {
var user = await users.findOne({ name: name });
return user || await createUser(name);
} |
function createUser(name) {
var response;
return regeneratorRuntime.async(function (context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return users.insert({ name: name });
case 2:
response = context$1$0.sent;
context$1$0.next = 5;
return users.findOne(response._id);
case 5:
return context$1$0.abrupt("return", context$1$0.sent);
case 6:
case "end":
return context$1$0.stop();
}
}, null, this);
} |
async function getOrCreateUser(name) {
var user = await users.findOne({ name: name });
return user || await createUser(name);
} |
function createUser(name) {
var response;
return regeneratorRuntime.async(function (context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return users.insert({ name: name });
case 2:
response = context$1$0.sent;
context$1$0.next = 5;
return users.findOne(response._id);
case 5:
return context$1$0.abrupt("return", context$1$0.sent);
case 6:
case "end":
return context$1$0.stop();
}
}, null, this);
} |
function getOrCreateUser(name) {
var user;
return regeneratorRuntime.async(function (context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return users.findOne({ name: name });
case 2:
user = context$1$0.sent;
context$1$0.t1 = user;
if (context$1$0.t1) {
context$1$0.next = 8;
break;
}
context$1$0.next = 7;
return createUser(name);
case 7:
context$1$0.t1 = context$1$0.sent;
case 8:
return context$1$0.abrupt("return", context$1$0.t1);
case 9:
case "end":
return context$1$0.stop();
}
}, null, this);
} |
async function createUser(name) {
var response = await users.insert({ name: name });
return await users.findOne(response._id);
}
async function createUser(name) {
var response = await (await users).insert({ name: await name });
return await (await users).findOne(await response._id);
}
function createUser(name) {
var response;
return regeneratorRuntime.async(function (context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return users;
case 2:
context$1$0.next = 4;
return name;
case 4:
context$1$0.t0 = context$1$0.sent;
context$1$0.next = 7;
return context$1$0.sent.insert({
name: context$1$0.t0
});
|
case 7:
response = context$1$0.sent;
context$1$0.next = 10;
return users;
case 10:
context$1$0.next = 12;
return response._id;
case 12:
context$1$0.t1 = context$1$0.sent;
context$1$0.next = 15;
return context$1$0.sent.findOne(context$1$0.t1);
case 15:
return context$1$0.abrupt("return", context$1$0.sent);
case 16:
case "end":
return context$1$0.stop();
}
}, null, this);
} |
Fibers
Minimizing the use of mutable shared global state is a good idea in general, of course.
One particular instance of shared global state that may be difficult to avoid: the file system.
Fibers
fiberHelpers.noYieldsAllowed(function () {
// Any calls to Fiber.yield will throw.
});
This could be improved by restricting what kind of events can
run while the current Fiber is
yielding, rather than preventing the
Fiber from yielding at all.
We also wrap the functions in Node's fs module to
be asynchronous when
Fiber.current is defined and
synchronous otherwise.
Fibers is a
burden.
Other native modules might not work well
with Fibers.
Our ability to keep up-to-date with the latest Node could be impeded by changes to its native module API.
Fortunately we have plenty of warning, and we ship specific versions of Node and all our packages.