On Giving libRebol JS more powers than JavaScript

It seems modest to imagine adding a function to a browser's JavaScript, which looped through two strings, printed both, and gave you back the second to display in a browser <div>:

var second = rebSpell(
    "repeat 2 [data: ask text!, echo [You typed: @data]] data"
)
console.log("Second string entered was" + second)

(Let's say the place it's carrying out the input and printing communication is in some kind of textedit-like control, and that is implicit.)

What could be simpler than that?!

Web browsers aren't designed to work this way :frowning:

Nothing about "JavaScript-the-language" particularly prohibits it from doing things synchronously. You find various synchronous APIs here and there... console.log() will write synchronously to the console, and alert() famously (and irritatingly) gives you a modal popup that will freeze everything until it's dismissed. Historically, you could even ask that an XMLHttpRequest be synchronous (a feature now disabled on the main thread and thus restricted to web workers).

So it isn't JavaScript with the limit--any more than C or anything else. It's the browser. While a JavaScript function is running on the main thread, the painting and mouse events and who-knows-what-else won't happen.

This isn't particularly new in the GUI app world...but it is particularly strict. Even Windows offered things like PeekMessage() to let you sneakily look through and process mouse messages without giving up control.

The tightening grip of the browser makers is speaking loud and clear:

  • there are some things only the GUI thread will ever be able to do
  • none of those things will be synchronous on the GUI thread--you can ask for work to be done, but will always be called back later with any result

So the code above is actually impossible?

Well, it's almost impossible. If you use Window.prompt() you can get input in an alert-box style modal dialog. And if you use console.log or Window.alert() for PRINT, it could "work"...but no one wants that.

Note what the "impossible" part of it is--the expectation that it runs and can output the result synchronously. If you were willing to instead write something like:

rebSpellCallback(
    "repeat 2 [data: ask text!, echo [You typed: @data]] data",
    function (second) {
        console.log("Second string entered was" + second)
    }
)
// any residual code here happens *before* the console.log output

Then that could be made to work. And since JavaScript programmers noticed over time that their language was a callback-reductio-ad-absurdum, they tried to address it with something called ASYNC/AWAIT:

async function whatever() {
   var second = await rebSpellPromise(
        "repeat 2 [data: ask text!, echo [You typed: @data]] data"
   )
   console.log("Second string entered was" + second)
   // any residual code here happens *after* the console.log output
}

In most ways this is just a syntax trick for the previous code pattern, so you don't have to write a cascading line of callbacks for every step in a sequence.

It looks good. BUT since it has to do some amount of "unwinding" to achieve its illusion, it has the unappealing property that if you ever call an async function from one that is not marked async (or if you leave off the AWAIT), the illusion is broken. You will just get a return object that's a "promise"...and the code after the promise will run before the consequences you were "awaiting" happen...just as if you'd had it trailing outside the place where you passed a callback function.

Wouldn't JS programmers just accept rebPromise()?

For larger operations, they probably wouldn't blink. They'd presumably be upset if JavaScript's addition required a callback to get the sum. But I can't honestly I say I know where their line is...and wouldn't be shocked if there was a BigNum library where they said the reason you had to wait for a callback for the sum was that it could be a potentially long operation and they didn't want to block the GUI, so it was dispatching it to a worker and would call you back when done. (?)

As an integral part of its variadic design, libRebol doesn't actually have any dedicated "small" operations. There's no separate atomic "give me the spelling of this REBVAL that is known to be an ANY-STRING!, and do only that", you can always throw in extra code. Even just asking to PROBE a string before returning its spelling would count, so long as its based on a DOM-manipulating PRINT:

 var sync_possible = rebSpell(some_str)
 console.log("spelling is: " + sync_possible);

 var sync_impossible = rebSpell("probe", some_str)
 // wouldn't be guaranteed that print was done by this line
 // if probe is based on synchronous-seeming print, sequencing lost

What I've thought of as a compromise would be to add rebPromise(), but still offer all the other APIs. Then have the other APIs trigger a failure if at runtime they discover they require synchronous operations...and point you to use rebPromise. If you're using async functions you'd say:

var value = await rebPromise("probe", str);
var sync_illusion = rebSpell(value);
rebRelease(value);

Simplifyable to:

var sync_illusion = rebSpell(rebR(await rebPromise("probe", str)));

While you can't abstract away the AWAIT (it's part of the language) I think we could probably get a pre-released promise result with a new API "instruction":

var sync_illusion = rebSpell(await rebP("probe", str));

So if you're using modern JavaScript and async functions, you could probably just inject await rebP( ) into the existing API set of operations, if it turned out what you were doing required it.

I'll quickly point out that if it seems having to do results with callbacks seems onerous, that the more modern an API in JavaScript is, the more of them they have. If you look at something like the fetch() API, not only do they have to "wait on" the server to return a response, the response itself then breaks down into more pieces you have to wait on! From a fetch example

fetch('viper.ogg')
  .then(function(response) { return response.arrayBuffer(); })
  .then(function(buffer) {
    audioCtx.decodeAudioData(buffer, function(decodedData) {
      source.buffer = decodedData;
      source.connect(audioCtx.destination);
    });
  });
};

But not all cases are pathological

Another angle to look at this is that trying to write an interactive REPL for a language running a synchronous script is actually a really hard example, and may make me worry more than I should.

A lot of projects would probably be fine having their PROBE and DUMP operations output to console.log, and not need synchronous interaction with some control that's part of the DOM. Many interactions with the DOM are fire-and-forget...expected to just update when they update...a small snippet of code runs, and there's no requirement that the updates be completed before anything else happens.

There may be a number of cases where someone might want to use Rebol as part of their web app and never expect it to do any I/O at all. I don't really know. It's all very speculative at the moment!

2 Likes