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();
});
};
}
Fiber
s!
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 Promise
s 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 Fiber
s,
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.
Fiber
s in the
browser is difficult.
For a framework that strives to be isomorphic, this inconsistency is unfortunate.
Fiber
s
using ES7 async
and
await
?
Technically yes, though it would
require await
ing 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 await
ed, 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);
} |
Fiber
s
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.
Fiber
s
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.
Fiber
s is a
burden.
Other native modules might not work well
with Fiber
s.
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.