QBasic INKEY in Javascript - an exploration of old and new
Intro
When I was a kid, computers didn't have multithreading, multitasking or even multiple processes. You executed a program and it was the only program that was running. Therefore the way to do, let's say, user key input was to check again and again if there is a key in a buffer. To give you a clearer view on how bonkers that was, if you try something similar in Javascript the page dies. Why? Because the processing power to look for a value in an array is minuscule and you will basically have a loop that executes hundreds of thousand or even millions of times a second. The CPU will try to accommodate that and run at full power. You will have a do nothing loop that will take the entire capacity of the CPU for the current process. The browser would have problems handling legitimate page events, like you trying to close it! Ridiculous!
Bad solution
Here is what this would look like:
class QBasic {
constructor() {
this._keyBuffer=[];
// add a global handler on key press and place events in a buffer
window.addEventListener('keypress', function (e) {
this._keyBuffer.push(e);
}.bind(this));
}
INKEY() {
// remove the first key in the buffer and return it
const ev = this._keyBuffer.shift();
// return either the key or an empty string
if (ev) {
return ev.key;
} else {
return '';
}
}
}
// this code will kill your CPU and freeze your page
const qb = new QBasic();
while (qb.INKEY()=='') {
// do absolutely nothing
}
How then, should we port the original QBasic code into Javascript?
WHILE INKEY$ = ""
' DO ABSOLUTELY NOTHING
WEND
Best solution (not accepted)
Of course, the best solution is to redesign the code and rewrite everything. After all, this is thirty year old code. But let's imagine that, in the best practices of porting something, you want to find the first principles of translating QBasic into Javascript, then automate it. Or that, even if you do it manually, you want to preserve the code as much as possible before you start refactoring it. I do want to write a post about the steps of refactoring legacy code (and as you can see, sometimes I actually mean legacy, as in "bestowed upon by our forefathers"), but I wanted to write something tangible first. Enough theory!
Interpretative solution (not accepted, yet)
Another solution is to reinterpret the function into a waiting function, one that does nothing until a key is pressed. That would be easier to solve, but again, I want to translate the code as faithfully as possible, so this is a no-no. However, I will discuss how to implement this at the end of this post.
Working solution (slightly less bad solution)
Final solution: do the same thing, but add a delay, so that the loop doesn't use the entire pool of CPU instructions. Something akin to Thread.Sleep in C#, maybe. But, oops! in Javascript there is no function that would freeze execution for a period of time.
The only thing related to delays in Javascript is setTimeout, a function that indeed waits for the specified interval of time, but then executes the function that was passed as a parameter. It does not pause execution. Whatever you write after setTimeout
will execute immediately. Enter async/await, new in Javascript ES8 (or EcmaScript 2017), and we can use the delay
function as we did when exploring QBasic PLAY:
function delay(duration) {
return new Promise(resolve => setTimeout(resolve, duration));
}
Now we can wait inside the code with await delay(milliseconds);
. However, this means turning the functions that use it into async
functions. As far as I am concerned, the pollution of the entire function tree with async keywords is really annoying, but it's the future, folks!
Isn't this amazing? In order to port to Javascript code that was written in 1990, you need features that were added to the language only in 2017! If you wanted to do this in Javascript ES5 you couldn't do it! The concept of software development has changed so much that it would have been impossible to port even the simplest piece of code from something like QBasic to Javascript.
Anyway, now the code looks like this:
function delay(duration) {
return new Promise(resolve => setTimeout(resolve, duration));
}
class QBasic {
constructor() {
this._keyBuffer=[];
// add a handler on every key press and place events in a buffer
window.addEventListener('keypress', function (e) {
this._keyBuffer.push(e);
}.bind(this));
}
async INKEY() {
// remove the first key in the buffer and return it
const ev = this._keyBuffer.shift();
// return either the key or an empty string
if (ev) {
return ev.key;
} else {
await delay(100);
return '';
}
}
}
const qb = new QBasic();
while (qb.INKEY()=='') {
// do absolutely nothing
}
Now, this will work by delaying for 100 milliseconds when there is nothing in the buffer. It's clearly not ideal. If one wanted to fix a problem with a loop running too fast, then the delay function should have at least been added to the loop, not the INKEY function. Using it like this you will get some inexplicable delays in code that would want to use fast key inputs. It's, however, the only way we can implement an INKEY function that will behave as close to the original as possible, which is hiring a 90 year old guy to go to a letter box and check if there is any character in the mail and then come back and bring it to you. True story, it's the original implementation of the function!
Interpretative solution (implementation)
It would have been much simpler to implement the function in a blocking manner. In other words, when called, INKEY would wait for a key to be pressed, then exit and return that key when the user presses it. We again would have to use Promises:
class QBasic {
constructor() {
this._keyHandler = null;
// instead of using a buffer for keys, keep a reference
// to a resolve function and execute it if it exists
window.addEventListener('keypress', function (e) {
if (this._keyHandler) {
const handler = this._keyHandler;
this._keyHandler = null;
handler(e.key);
}
}.bind(this));
}
INKEY() {
const self = this;
return new Promise(resolve => self._keyHandler = resolve);
}
}
const qb = new QBasic();
while ((await qb.INKEY())=='') { // or just await qb.INKEY(); instead of the loop
// do absolutely nothing
}
Amazing again, isn't it? The loops (pun not intended) through which one has to go in order to force a procedural mindset on an event based programming language.
Disclaimer
Just to make sure, I do not recommend this style of software development; this is only related to porting old school code and is more or less designed to show you how software development has changed in time, from a period before most of you were even born.
Comments
Be the first to post a comment