Why Fibers Make Sense For Meteor

Ben Newman (Meteor)
11 May 2015

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


benjamn.github.io/goto2015-talk

How do
languages
change?

How do
programming languages
change?

How do
natural languages
change?

How can
JavaScript
change?

“The Web is a brutal,
shortest-path, Worse-is-Better evolving system.”

JavaScript is finally
becoming the language
we want.

Many new features can
be simulated without any
changes to the language.

// 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; })

But not everything!

Today I want to talk about a powerful feature that cannot be simulated in any reasonable way.


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.

And you can use this feature in Meteor today!

Coroutines (Fibers)


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, that's exactly what coroutines are for.

Coroutines (Fibers)


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(); }

Coroutines (Fibers)


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(); }

Many other languages
support coroutines!


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

Many other languages
support coroutines!


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

Why not JavaScript?

Dave Herman (Mozilla, TC39):

Programming language design is as much about saying “no” to tempting features as it is about saying “yes.”

If we can agree that making asynchronous programming easier is important for the future of JavaScript, and TC39 is not interested in coroutines, then what other remedy do they have in mind?

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

Let's try that with pure promises...

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); });

Let's try that with pure promises...

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); });

Let's try that with pure promises...

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!

What about callbacks?

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); }); }

What about callbacks?

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); }); }

What about callbacks?

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); }); }

We've come so far!

Can we do better?

If JavaScript had coroutines, we could implement 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); }

If JavaScript had coroutines, we could implement 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); }

If JavaScript had coroutines, we could implement 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); }

If JavaScript had coroutines, we could implement 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); }

If JavaScript had coroutines, we could implement 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); });

If JavaScript had coroutines, we could implement 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); });

If JavaScript had coroutines, we could implement 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!

How might these magical functions be implemented?

function await(argument) { }

How might these magical functions be implemented?

function await(argument) { var fiber = Fiber.current; }

How might these magical functions be implemented?

function await(argument) { var fiber = Fiber.current; assert.ok( fiber instanceof Fiber, "Cannot await without a Fiber" ); }

How might these magical functions be implemented?

function await(argument) { var fiber = Fiber.current; assert.ok( fiber instanceof Fiber, "Cannot await without a Fiber" ); return Fiber.yield(); }

How might these magical functions be implemented?

function await(argument) { var fiber = Fiber.current; assert.ok( fiber instanceof Fiber, "Cannot await without a Fiber" ); Promise.resolve(argument) return Fiber.yield(); }

How might these magical functions be implemented?

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(); }

How might these magical functions be implemented?

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(); }

How might these magical functions be implemented?

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(); }

How might these magical functions be implemented?

function async(fn) { }

How might these magical functions be implemented?

function async(fn) { return function () { }; }

How might these magical functions be implemented?

function async(fn) { return function () { var self = this; var args = arguments; }; }

How might these magical functions be implemented?

function async(fn) { return function () { var self = this; var args = arguments; return new Promise(function (resolve, reject) { }); }; }

How might these magical functions be implemented?

function async(fn) { return function () { var self = this; var args = arguments; return new Promise(function (resolve, reject) { new Fiber(function () { }) }); }; }

How might these magical functions be implemented?

function async(fn) { return function () { var self = this; var args = arguments; return new Promise(function (resolve, reject) { new Fiber(function () { fn.apply(self, args) }) }); }; }

How might these magical functions be implemented?

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)); }) }); }; }

How might these magical functions be implemented?

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) { } }) }); }; }

How might these magical functions be implemented?

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); } }) }); }; }

How might these magical functions be implemented?

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(); }); }; }

Once we have these two functions in our toolchain, we can forget they were implemented using Fibers!

These two functions are more powerful than they seem.

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

These two functions are more powerful than they seem.

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.

These two functions are more powerful than they seem.

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.

These two functions are more powerful than they seem.

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

These two functions are more powerful than they seem.

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!

These two functions are more powerful than they seem.

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.”

It can still be valuable to mark functions as 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.

How does Meteor allow
database access on the client?


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.

How should Meteor allow
database access on the client?


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.

Summary


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.

Summary


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); }

Summary


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); }

Summary


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.

Summary


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.

Summary


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.

There has to be a catch, right?

Catch #1: Simulating Fibers in the browser is difficult.


For a framework that strives to be isomorphic, this inconsistency is unfortunate.

Could we implement 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.

And why would that be bad?

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); }

And why would that be bad?

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); }

And why would that be bad?

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); }

And why would that be bad?

async function createUser(name) { var response = await users.insert({ name: name }); return await users.findOne(response._id); }

And why would that be bad?

async function createUser(name) { var response = await (await users).insert({ name: await name }); return await (await users).findOne(await response._id); }

And why would that be bad?

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); }

Catch #2: Mutating shared global state is tricky with 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.

How we write safe file system code with 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.

Catch #3: Ensuring compatibility with 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.

For Meteor, the benefits are clear:


  • Learnability

  • Maintainability

  • Flexibility

Thanks!