Interaction in Deltaphone
One of the many superpowers of a musician is the ability to hear intervals. This is not a superpower I possess. Can it be learned? While I’ve been working on Deltaphone, a friend mentioned that he coded an interval generator when he was a kid in order to help him train his ear. Last week I had the chance to visit a music theory club at an intermediate school, and they played a game in which a teacher played an interval on the piano and the students identified it.
Yesterday I set about adding support to Deltaphone for generating this kind of interactive game so that I too could acquire this superpower. But Deltaphone wasn’t exactly designed with interaction in mind. Previously, I interpreted the program completely before processing and rendering the score. But in a game, the interpreter might need to render the score more frequently to prompt the player or give feedback. Furthermore, the interpreter must occasionally wait for user input before proceeding. How would I stop the interpreter?
I feared I would need to rewrite the interpreter from scratch to support some form of continuations. But then I discovered Javascript’s async
and await
commands. Tagging a function with async
makes it run asynchronously. An async
function immediately returns a promise to its caller, at which point the caller normally continues on its merry way. To cause execution to block, however, one can await
the resolution of a promise. Using these two commands, one can effectively suspend a function’s return without a busy loop.
Suppose I want to pause the interpreter until the user clicks a button. I added this wait-for-click statement, which only resolves its promise on the button’s click event:
class StatementWaitForClick() {
async evaluate(env) {
await new Promise(resolve => {
button.onclick = () => resolve();
});
}
}
Very few of my structures need an explicit promise—only wait-for-click and multiple-choice-quiz. But I had to tag all my evaluate
functions across my abstract syntax tree with async
because I never know if a substructure might pause during its evaluation. This means that even those operations without an explicit promise must still await the asynchronous evaluation of their substructures, as we do in this add operation:
class ExpressionAdd(a, b) {
constructor(a, b) {
this.a = a;
this.b = b;
}
async evaluate(env) {
let valueA = await a.evaluate(env);
let valueB = await b.evaluate(env);
return valueA + valueB;
}
}
With a pausable interpreter, Deltaphone programmers can now build interactive games. Like this one for recognizing intervals:
Just now I got 4 out of 5!