Easily
(Some of)
The Weirdest Code I've Written, or:
Continuation-Based Web Framework
in JavaScript,
Part 0
TL;DR: I was trying to implement first-class continuation in a language that doesn't support it (JavaScript). I failed. I did come across a weird party trick though.
The road to continuation (or the lack thereof)
The web framework of Racket is continuation-based. No one really writes backend that way anymore (unless you're one of the rare ones that uses Seaside) because languages where you can fully do this trick are rare. Unlike Racket, JavaScript technically does not have any continuation-related primitives. I tried to read other people's papers on continuation including those of Oleg Kiselyov attempting to find a way to add that in as a library that wouldn't require me to change the language but I couldn't understand a single thing due to skill issue. At the end I have to stay away from the idea of "implementing continuation".
I decided to think about how one can stop and resume at the middle of a function instead. In Python this would be easy because it has yield statement, but at the time I thought JavaScript didn't have that, and which was wrong because yield was introduced to JavaScript with ES2015… but let's assume it's not the case for now because that's how my thinking process went. One way to mimic early return (like yield) is to actually return early and wrap everything that comes after into a callback like this:
function coroutineA() { doSomething1(); yieldWithCallback(() => { doSomething2(); yieldWithCallback(() => { doSomething3(); // ... }); }); }
Some of the more PLT-savvy kind among you should've noticed this is continuation-passing style. Asking people to write this kind of code manually clearly won't do: if I remembered correctly this was how people did it in the very early days of Node.js; they called it the "callback hell" and everyone hated it.
Thus I try to think of something Promise-like like this so at least things won't get nested too deep:
const coroutineA = new SomeObjectThatDoesThings(() => { doSomething1(); }).andThen(() => { doSomething2(); }).andThen(() => { doSomething3(); }) // ...
And you can hide whatever you need to do in yieldWithCallback in SomeObjectThatDoesThings.andThen. I was not happy with this either because for whatever reasons I had this gut feeling that splitting what should be one coherent thing into many tiny pieces of things is bad fengshui. I thus continue by thinking how I can make it work with async and await, and I managed to come up something like this:
// the names are from Racket. function sendBack(s) { let self = this; return new Promise((resolve, reject) => { self.res.write(s); self.res.end(); resolve(); }); } function sendSuspend(f) { let self = this; return new Promise((resolve, reject) => { let nextUrl = generateUniqueUrl(); self.res.write(f(nextUrl)); self.res.end(); let done = false; registerCallback(nextUrl, (req, res) => { self.res = res; resolve(req); }); }); } function sendSuspendDispatch(f) { let self = this; return new Promise((resolve, reject) => { self.res.write( f((cb) => { let nextUrl = generateUniqueUrl(); registerCallback(nextUrl, (req, res) => { self.res = res; resolve(cb(req)); }); return nextUrl }) ); self.res.end(); }); }
The idea is that await-ing a Promise suspends the execution of the current function for that Promise until that Promise explicitly resolved or rejected; so during the execution of that Promise I can dynamically register a callback to a route that triggers the resolving. This way, from the viewpoint of the "await-er", it would look like you're truly waiting for some kind of computation like waiting for a database query.
With the following "continuation manager":
let dispatch = { }; function registerCallback(dispatchId, f) { dispatch[dispatchId] = f; } const urlCharacterSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; function generateUniqueUrl() { var res = []; for (let i = 0; i < 32; i++) { let selection = Math.floor(Math.random()*urlCharacterSet.length); res.push(urlCharacterSet[selection]); } return res.join(""); } // ... // you can figure out the routing on your own. dispatch[someRoute] = routeHandler; let x = http.createServer((req, res) => { let dispatchId = req.url.substring(1); if (!dispatch[dispatchId]) { dispatchId = ''; } dispatch[dispatchId](req, res); }); x.on('clientError', (err, socket) => { socket.write("HTTP/1.1 400 Bad Request"); }); x.listen(8000);
The "operators" can be used like this:
async function example1(req, res) { let dispatcher = {res: res}; let ss = sendSuspend.bind(dispatcher); let sb = sendBack.bind(dispatcher); let firstResponse = await ss((returningurl) => { return ` <html> <body> <form method="POST" action="${returningurl}"> <input name="number" /> <input type="submit" /> </form> </body> </html>`; }); let firstResponse = await firstResponseRaw; let first = parseBodyToHashMap(await readRequestBody(firstResponse)); console.log("first ", first); let firstNumber = parseFloat(first.number); let secondResponse = await ss((returningurl) => { return ` <html> <body> <form method="POST" action="${returningurl}"> <input name="number" /> <input type="submit" /> </form> </body> </html>`; }); let second = parseBodyToHashMap(await readRequestBody(secondResponse)); console.log("second ", second); let secondNumber = parseFloat(second.number); let thirdResponse = await ss((returningurl) => { return ` <html> <body> <form method="POST" action="${returningurl}"> <input name="number" /> <input type="submit" /> </form> </body> </html>`; }); let third = parseBodyToHashMap(await readRequestBody(thirdResponse)); console.log("third ", third); let thirdNumber = parseFloat(third.number); await sb(` <html> <body> <h1>the sum is ${firstNumber + secondNumber + thirdNumber}</h1> </body> </html>`); }
async function example2(req, res) { let dispatcher = {res: res}; let ssd = sendSuspendDispatchRaw.bind(dispatcher); let sb = sendBack.bind(dispatcher); let i = 10; while (true) { let resp = await ssd((toUrl) => { return ` <html> <body> <h1>Current value is ${i}</h1> <a href="${toUrl((req) => { i += 1; return null; })}"> add 1 </a><br > <a href="${toUrl((req) => { i -= 1; return null; })}"> minus 1 </a><br /> </body> </html>`; }); if (i < 0) { break; } } await sb(` <html> <body> <h1>exited.</h1> </body> </html>`); }
JavaScript has yield; could we do it with yield?
I haven't tried but I suppose you can, although I'd imagine the merit of using generators over async functions are marginal if not non-existent because one big thing people don't like about async is that it's a color and generator is also a color; you'd still have to choose between CPS manually, allowing the color to spread (perhaps with manual written boundaries), or ditching the idea altogether and rely on language-level transforms for continuations.
One very big catch
We only managed to suspend and resume computations; we never captured anything. It's like the web-server/managers/none in Racket, but instead of capturing continuations and choosing not to store them, our sendSuspend and sendSuspendDispatch is just simply crippled and not being able to capture anything at all. Whatever URLs we've generated cannot be visited again. I tried to think of a way to solve this, but I couldn't think of any; I believe it's impossible since we never passed any information about "what comes after" into sendSuspend and sendSuspendDispatch (this is why the manual CPS thing comes into play, because if we write "what comes after" as a normal function then it can be easily passed around).
What comes next
Two choices:
- I shall find a way to make manual CPS relatively palatable.
- I shall make a transpiler that does the necessary transforms.
I'll try both in the future…
Last update: 2025.3.26