Implementing async/await in javascript with Sweet.js
Ever since I went back to Javascript, I’ve been missing C#’s async/await system.
With C# you can mark some methods async. Afterwards, you can call them “synchronously” using the await keyword. Behind the scenes, the compiler rewrites the code to use callbacks.
For example, C# lets you write something like this:
var status = await $.get("/login"); // await returns a status object.
if (status.success) {
// whatever
}And it gets compiled to this:
$.get("/login").done(function(status) {
if (status.success) {
// whatever
}
}).fail(function(status) {
if (status.success) {
// whatever
}
});To me, it’s a net win in terms of readability because I’ve never really liked callbacks. They’re an implementation detail and I’d rather have them swept under the rug.
Last week I heard of sweet.js, a macro compiler for Javascript, so I decided to try to implement some sort of await functionality with macros.
The basics of Sweet.js
Sweet.js lets you define syntaxic sugar, i.e, macros which get expanded at compile time.
A sweet.js macro has two parts, a matching expression and a replacement expression.
Let’s go back to C macros because sweet’s macros will be easier to understand this way. In C, a macro is a statement which defines an expression and a replacement for this expression. During the compilation, the macro is expanded by the preprocessor.
Here’s the simplest macro of all, a greet function.
#define greet(name) printf("hello #name")This is the equivalent sweetjs macro:
macro greet {
rule {
($name:lit)
} => {
/* replace everything with this text. */
console.log("hello " + $name);
}
}
greet('Paul'); // output: 'console.log('hello ' + 'Paul');'There’s several important things.
-
First you can see sweet has a little more syntax than the C preprocessor. A macro is defined inside a
macroblock. Inside this block you can have several rules of the form{ pattern } { replacement }. -
The rules for matching macros are similar to regexps. The block
($name:lit)means: “match one litteral between two parentheses and assign it to the variable$name”. -
The replacement block contains the replacement text and can optionally call javascript code.
So, we’ve got a macro and it gets expanded at compile-time. However, because of the way we wrote it, it will trigger an error if we pass it more than one argument. Let’s fix this.
Handling multiple parameters
Sweetjs has a specific syntax for defining variadic macros: a ... will match any token similar to the previous match:
macro greet {
rule {
($name:lit (,) ...) // "(,)" means don't capture commas.
} => { console.log("hello ", $name (,) ...)}
}
greet('Paul', 'Ringo', 'George');
// output: console.log('hello ', 'Paul', 'Ringo', 'George');Again, here are some important changes:
- We don’t want to match commas, so ask the compiler to ignore them by using
(,). - The ‘…’ token itself. The tokens matched are exposed in the replacement rule as
....
Now, let’s try to define a small macro to do simplify those messy jQuery ajax calls.
Handling async AJAX calls
The jQuery guys, knowing how painful it is to use callbacks, have defined a promise interface. It allows us to chain calls in a pseudo-procedural type:
$.get({"/api.json"})
.done(function(data) { })
.fail(function(error) { });I still find this too annoying. Let’s simplify things with this macro:
let var = macro {
rule {
$result = await $.get($x);
$rest
...
} => {
var $result = $.get($x).done(function($result) {
$rest
...
}).fail(function($result) {
$rest
...
});
}
}
var result = await $.get({url: "http://google.com"});
console.log(result);This compiles to the following javascript:
var result$511[^hygienic] = $.get({ url: 'http://google.com' }).done(
function (result$512) {
console.log(result$512);
}).fail(function (result$513) {
console.log(result$513);
});There’s only two major changes between the greet macro and this one:
- We’re using variadic parameters differently than in the previous example: instead of capturing function parameters we use them to capture every token after the macro.
- We’re redefining
varas a macro. This lets us expands constructs likevar result = await $.get. To get more specific, we’re redefiningvaras an anonymous macro because we don’t want sweet to try to expand everyvarinside the macro.
By the way, do you wonder where these weird result$511 and al come from? It’s because sweet macros are hygienic: when they get expanded, identifiers are renamed to prevent name clashes.