Promises, Promises: Should THEN and ELSE be used for them?

Tinkering with JavaScript again after some years of being blissfully ignorant, I notice that a style of programming which used to be popular there has caught on even more. These are "Promises"--which I've seen under other names like "Futures".

They've become so ingrained that they're now in the JavaScript language itself:

(Note: Worth pointing out is that there's a fundamental feature missing from the built-in promises, namely cancellation. Kind of makes you roll your eyes when you see someone deploy a major language feature to a worldwide audience--as if it's "the future of asynchronous programming and network requests"--when there's no way to cancel.)

When you have a promise in your hand, you can hook it up to clauses that will run if it succeeds, and clauses to run if it fails. You hook them together with successive function calls...where the THEN clauses are the successes, and the CATCH clauses are the failures:

some_promise.then(
    function(val) {
        log.insertAdjacentHTML('beforeend', val +
            ') Promise fulfilled (<small>Async code terminated</small>)<br/>');
    }).catch(
    // Log the rejection reason
   (reason) => {
        console.log('Handle rejected promise ('+reason+') here.');
    });

You can make loooong chains out of these. Steps can be triggered by callbacks, or even by just an ordinary non-promise value falling through immediately from the previous step. It may help to look at a big example (from this article, if it looks weird with const and => remember that JavaScript has changed a bit, and this stuff is in it now):

const wait = time => new Promise(
   res => setTimeout(() => res(), time)
);

wait(200)
   // onFulfilled() can return a new promise, `x`
  .then(() => new Promise(res => res('foo')))
  // the next promise will assume the state of `x`
  .then(a => a)
  // Above we returned the unwrapped value of `x`
  // so `.then()` above returns a fulfilled promise
  // with that value:
  .then(b => console.log(b)) // 'foo'
  // Note that `null` is a valid promise value:
  .then(() => null)
  .then(c => console.log(c)) // null
  // The following error is not reported yet:
  .then(() => {throw new Error('foo');})
  // Instead, the returned promise is rejected
  // with the error as the reason:
  .then(
    // Nothing is logged here due to the error above:
    d => console.log(`d: ${ d }`),
    // Now we handle the error (rejection reason)
    e => console.log(e)) // [Error: foo]
  // With the previous exception handled, we can continue:
  .then(f => console.log(`f: ${ f }`)) // f: undefined
  // The following doesn't log. e was already handled,
  // so this handler doesn't get called:
  .catch(e => console.log(e))
  .then(() => { throw new Error('bar'); })
  // When a promise is rejected, success handlers get skipped.
  // Nothing logs here because of the 'bar' exception:
  .then(g => console.log(`g: ${ g }`))
  .catch(h => console.log(h)) // [Error: bar]
;

This resembles the kind of chaining done with enfix, so...

...might THEN and ELSE be promise-aware?

JavaScript uses the name THEN here to mean "do this thing on the right if the left happens" or "if the left is fulfilled". You can get a "false" result back from a promise:

var falsify = new Promise(
   resolve => {
       resolve(false) // calling resolve will fulfill the promise
   }
);

falsify.then(x => console.log(x)) // this will print out false

Here we have to ask a question about what the rules are for the system. If you see something like:

all [...] then [print "done with all that stuff"]
print "moving on..."

Would you want the system to be based on a worldview where that could output:

"moving on..."
"done with all that stuff"

If you have a PROMISE! datatype in the picture, then programs which use them will become asynchronous even if they don't appear to be written in an asynchronous style. The more automatic it is, the greater the risks become.

JavaScript's promises involve very little system support. Not only were most promise library designs done before the language supported them at all, one of the most popular implementations doesn't even use the new JS built-in promises...but cooks up its own using functions and objects.

So this is different from what Gabriele was suggesting when he brought up promises as a likely path forward for PORT!s in his Topaz design notes. He's suggesting that a PROMISE! would be an active datatype, where if you merely said data: read url and got a promise back, then length of data, it would automatically wait until the length was known. You wouldn't have to explicitly WAIT on it (or have a THEN, as suggested above).

It's trying to suggest a synchronous client of READ could go on their merry way using it just as if they got bytes back, or tactically drill in with promise-aware constructs on an as-needed basis. As nice as that sounds it's not realistic. What you'd need to accomplish such a goal more or less asks the runtime support for lazy evaluation, and the restrictions that brings in would push back to where the user's style of programming would have to morph into something much more like Haskell than like Rebol/JavaScript/etc.

JavaScript's answer is to quarantine down the promise-aware constructs of the language to more or less zero. Their if() treats them as truthy, not "wait on them asynchronously and see if the result was truthy, or falsey if an error". Things like .then() are methods on the promise itself, not generic language operators.

As I've pointed out that people aren't even necessarily with the built-in promises of JavaScript--enough to continue using libraries that cobble the protocols together out of objects and functions--it may suggest that zero is not a good sweet spot for language support. To really be compelling enough for pervasive use there may need to be some special sauce you don't get elsewhere (including, I'd have to say, cancellation).

I do wonder if conditionals could be a place where it might be good to synchronize a promise. Or if ANY and ALL could imply WAITs. But certainly it seems like it would be appealing if THEN and ELSE could be used...so I guess that's what I'll put out there for a starting thought.

Not sure if you looked at JavaScript's async/await notation as a "nicer" (or synchronous-looking) way to use promises or asynchronous operations. Would something similar make sense for Ren-C? I mentioned these in the chat, I think, but you didn't comment.

Okay, well that revises the number of native JavaScript keywords for promises to, like, two.

https://javascript.info/async-await

So yes, in Rebol it would make sense to assign that to WAIT. I was talking about that kind of thing, but also whether there are implicit waits in the system, or if it's always explicit.

We have sort of idealized being able to write things like:

if parse read http://example.com [...] [
    ...
]

But complexity rears its head and you start saying "well, how does it know if you want the UTF-8 bytes or if you want it to decode the URL as an IMAGE!" or whatever. Is READ always bytes, and LOAD is higher level? READ/STRING is certainly very limited and weird...refinements don't seem like an answer.

So maybe READ is byte-oriented, so that's LOAD which is higher level and does more sniffing to check the content-type. But now, we ask if the asynchronous situation is such that synchronous clients have to say:

if parse wait load http://example.com [...] [
    ...
]

Questions we start pushing around in our head is whether or not a synchronous client should have to put an extra word in there or not. WAIT is a short word and keeps things explicit--it's not as far-out-there as Gabriele's imagining. In this scenario, if you forget the WAIT then PARSE just says it doesn't know how to parse promises (whatever their implementation is).

But the principal question I'm throwing in here for starters is if it's comfortable having THEN and ELSE serve a dualistic purpose of these promise handlings, as well as their current function. I imagine it is.


One thing that has to be confronted is that Rebol is not very efficient with:

 some-promise then function [x] [...lots of code...]
 some-promise then x => [...lots of code...]

Each time at source-level you make a function like that, it goes through the motions of creating a new ACTION! object, relativizing the body, etc. Making matters even worse, it will do this whole process whether the function is used or not.

While this looks terribly inefficient and lame for a static scenario, what makes it interesting is all the flexibility it permits with dynamically creating that function. Not having that dynamism sucks. You can avoid it with constructs you might offer people to use at an outer level in a prepass:

composeIII [
    ...
    ...
    some-promise then (((x => [...lots of code...])))
    ...
    ...
]

But if you need to capture things from the environment that won't work. This does make something like an await/WAIT better from an efficiency standpoint.

Anyway, there's a lot of stuff to contemplate, so if people want to work through examples and make lists and do analysis that would be helpful.