Skip to content

Instantly share code, notes, and snippets.

@getify
Created December 3, 2010 17:10
Show Gist options
  • Save getify/727232 to your computer and use it in GitHub Desktop.
Save getify/727232 to your computer and use it in GitHub Desktop.
theoretical native promise/defer via @ operator (in JavaScript or something like it)
//SUMMARY:
// a() @ b() ==> execute a(). if a() flags an async deferral inside it,
// then wait to continue execution of the expression until that promise is
// fulfilled, then continue execution *AT* b().
//
// more generally: X @ Y ==> evaluate X expression. if it was a function call
// call that deferred with a promise, wait until fulfilled then continue at
// Y. otherwise, assume that X had an implicit immediately fulfilled promise,
// and continue evaluating at Y.
// ------
// basically, the idea is that every function should be able to flag a "state"
// that is inspectable (by the operator) after the function's call completes,
// and that state (the "promise") indicates if the promise is immediately
// fulfilled, or if it's deferred to fulfill later. The @ operator inspects
// this special state of the function call to determine if it should wait or
// proceed with evaluation of the rest of the statement expression.
//
// NOTE: by modeling a function's promise as a special internal state of the
// function, rather than conflating it with the function's return value, the
// function can also return any value immediately, even if it's ultimately
// deferred to complete later.
function foo() {
setTimeout(function(){
blah++;
console.log(blah);
},1000);
}
var blah = 1;
foo(); blah = 10; foo(); // expected output: 11, 12
----------------
function foo() {
var p = promise; // `promise` being new auto keyword kinda like `arguments`
setTimeout(function(){
blah++;
console.log(blah);
p.fulfill();
},1000);
p.defer(); // flag this function as needing to defer its promise
}
var blah = 1;
foo() @ (blah = 10); foo(); // expected output: 2, 11
blah = 1;
foo() @ (blah = 10) @ foo(); // expected output: 2, 11
----------------
function foo() {
var p = promise; // `promise` being new auto keyword kinda like `arguments`
setTimeout(function(){
blah++;
console.log(blah);
p.fulfill();
},1000);
p.defer(); // flag this function as needing to defer its promise
}
var blah;
(blah = 5) @ foo() @ (blah = 10) @ foo(); blah = 100; // expected output: 101, 11
----------------
function foo() {
var p = promise; // `promise` being new auto keyword kinda like `arguments`
console.log(blah);
setTimeout(function(){
blah++;
console.log(blah);
p.fulfill();
},1000);
p.defer(); // flag this function as needing to defer its promise
}
var blah;
(blah = 5) @ foo(); blah = 100; // expected output: 5, 101
blah = 5; foo(); blah = 100; // expected output: 5, 101
blah = 5; foo(); blah = 100; foo(); // expected output: 5, 100, 101, 102
(blah = 5) @ foo() @ (blah = 100) @ foo(); // expected output: 5, 6, 100, 101
function foo() {
var p = promise; // `promise` being new auto keyword kinda like `arguments`
setTimeout(function(){
if (blah == 5) {
p.fulfill();
}
else {
p.fail();
}
},1000);
p.defer(); // flag this function as needing to defer its promise
}
function yay() {
console.log("Yay, blah is: "+blah);
}
function bummer() {
console.log("Bummer, blah is: "+blah);
}
var blah = 5;
foo() @ yay() : bummer(); // Yay, blah is: 5
foo() @ yay() : bummer(); blah = 10; // Bummer, blah is: 10
(blah = 5) @ foo() @ yay() : bummer(); // Yay, blah is: 5
blah = 5;
foo() @ (blah = 10) @ yay() : bummer(); // Yay, blah is: 10
blah = 5;
foo() @ (blah = 10) @ foo() @ yay() : bummer(); // Bummer, blah is: 10
blah = 5;
foo() @ ((blah = 10) @ yay() : bummer()) : bummer(); // Yay, blah is: 10
blah = 10;
foo() @ ((blah = 5) @ yay() : bummer()) : bummer(); // Bummer, blah is: 10
blah = 5;
foo() @ yay() : ((blah = 5) @ foo() @ yay() : bummer()) // Yay, blah is: 5
function foo() {
var p = promise; // `promise` being new auto keyword kinda like `arguments`
setTimeout(function(){
if (blah == 5) {
p.fulfill("`blah` was the correct value!");
}
else {
p.fail(2, "`blah` was an incorrect value.", blah);
}
},1000);
p.defer(); // flag this function as needing to defer its promise
}
function yay() {
console.log(promise.messages[0]);
}
function bummer() {
console.log(promise.messages[0]+": "+promise.messages[1]);
}
var blah = 5;
foo() @ yay() : bummer(); // `blah` was the correct value!
(blah = 10) @ foo() @ yay() : bummer(); // 2: `blah` was an incorrect value.
function onclick(obj,callback) {
obj.addEventHandler("click", callback, true);
}
var clicker = document.getElementById("clicker"),
btn = document.getElementById("btn")
;
onclick(elem,function(){
onclick(btn,function(){
console.log("clicker & button clicked");
});
});
--------------------------
function onclick(obj) {
var p = promise;
obj.addEventHandler("click", function(){
p.fulfill();
}, true);
p.defer();
}
var clicker = document.getElementById("clicker"),
btn = document.getElementById("btn")
;
onclick(elem) @
onclick(btn) @
function(){ console.log("clicker & button clicked"); };
function requestA() {
var p = promise;
// make some request remotely
// when it finishes, call p.fulfill(requestA_value);
// if it errors, call p.fail();
p.defer(); // signal this function as deferred completion
}
function requestB() {
var p = promise;
// make some request remotely
// when it finishes, call p.fulfill(requestA_value);
// if it errors, call p.fail();
p.defer(); // signal this function as deferred completion
}
--------------------------
// intersection (serial):
function intersection(A, B) {
function get_result() { return promise.messages[0]; }
var p = promise, resultA, resultB, result;
A() @ (resultA = get_result()) @ B() @ (resultB = get_result()) @ (function(){
result = resultA + resultB;
console.log(result);
p.fulfill(result);
});
p.defer();
}
intersection(requestA, requestB) @ function(){
console.log("Intersection: "+promise.messages[0]);
};
--------------------------
// intersection (parallel):
function intersection(A, B) {
function complete() {
var p = promise, result;
results.push(p.messages[0]);
if (results.length == 2) {
result = results[0] + results[1];
console.log(result);
p.fulfill(result);
}
}
var p = promise,
results = array()
;
// here, A() and B() will run in parallel, and `complete` will be the gate
A() @ complete();
B() @ complete();
p.defer();
}
intersection(requestA, requestB) @ function(){
console.log("Intersection: "+promise.messages[0]);
};
// the following is meant to explore a possible shim implementation
// (similar to how coffeescript compiles to uglier js) that could
// emulate the proposed @ behavior in current JS.
X @ Y
when(X, function() { Y; });
// but what if Y relies on the `this` or `arguments`?
function do_Y() {
Y;
}
var args = Array.prototype.slice.call(arguments);
args.unshift(this);
var bound_Y = do_Y.prototype.bind.apply(this,args);
when(X, bound_Y);
// what about if Y uses a `return`? Too complicated. I'd
// simply say the semantics of @ expressions are that the
// `rvalue` cannot be a `return` statement/expression.
// Because @ can suspend the containing statement, it can be used in control and loop structures, as well
if (X() @ true) {
// if X() completes immediately, this block will run
// if X() defers, the whole if-statement is paused.
// if X() eventually fulfills successfully, the @ expression
// will evaluate the `true`, and this block will then run
// if X() eventually fails, the @ expression will evaluate to
// `undefined` (falsy), and this block will NOT run
}
// OR, more explicitly if you like
if (X() @ true : false) {
// will be have the exact same way
}
// in a loop:
for (i=0; X() @ true; i++) {
// loop iteration each time that X() fulfills successfully,
// whether that's immediate or later
// stop when X() fails
}
@getify
Copy link
Author

getify commented Dec 3, 2010

to be clearer on the functionality of the @ operator in this usage:

the lvalue (the operand to the left of the @) will be evaluated. if the lvalue expression was a function call which deferred its internal promise (possible examples shown above), then the rvalue (operand to the right of the @) will wait until the lvalues promise is fulfilled before proceeding to evaluate the rvalue expression.

if the lvalue expression was not a function call who deferred its promise, such as a regular function call, or an immediate inline expression (like blah = 5), the @ operator will consider this an immediately fulfilled promise and will proceed directly to evaluate the rvalue expression.

@samsonjs
Copy link

samsonjs commented Dec 3, 2010

I completely agree that JS needs something like this built in. I'm not crazy about this syntax but I like that it's not nested and is more akin to Haskell's $ operator. I just don't find it very readable all crammed on one line, and we'd have to break lines after the @ operator because of ASI.

What if there were a new kind of block structure inside of which each statement was implicitly wrapped in a when and following statements were shifted into an implicit callback on that when. It sounds like a lot of implicit magic going on but I think the resulting code would be pretty nice.

Possibly something like this (using foo from example 4 above, which logs blah then increments & logs blah again after 1s):

// expected output: 5, 6, 100, 101
async {
  blah = 5;
  foo();
  blah = 100;
  foo();
}

I think these semantics are less confusing. The 3rd line in example 4 above would be written as follows:

// expected output: 5, 100, 101, 102
async {
  blah = 5;
  foo(); // logs 5, then 101
}

// the async statement doesn't block, unless there is long-running code like while(true){} inside it.

async {
  blah = 100;
  foo(); // logs 100, then 102
}

(Of course this still depends on some kind of built in promises)

edit: Promises would have to chain automatically so that if foo() returned another promise the async{} block containing the foo() call would have to wait on that promise. Twisted's deferred is a great example of this behaviour though. Hopefully I can open up our JS port of it soon.

@getify
Copy link
Author

getify commented Dec 3, 2010

that's an interesting idea. not sure i'd agree that "async" is the right word for such a construct... more like "defer" i would think. perhaps that's just me mentally wanting to confuse this "async" with the script tag's "async" attribute/property (which is heavily on my mind lately regarding LABjs).

the other thing i'm concerned about is that there are some times when you want both a "fulfilled" promise handling as well as a "broken" promise handling -- in other words, the if-then-else type scenario. i didn't show it above, for fear of confusing the initial discussion, but my idea for the @ operator is that it could optionally have a : paired with it to make it a special promises-ternary type behavior. something like:

foo() @ bar() : baz()

Also, not sure why you say that the @ operator couldn't be broken across lines without ASI issues? the ternary operator works just fine across lines, right?

foo()
   @
bar()

should work i would think, just like:

a
   =
2
   +
3;

works fine.

@samsonjs
Copy link

samsonjs commented Dec 3, 2010

Right you are. I hadn't thought about the ternary or assignment operators, I was seeing it as an infix op.

The @ still seems arbitrary and doesn't convey much information as to what the operator does. However it is useful for making async expressions instead of statements. I'm a bit torn on that.

If we had defer perhaps it could accept a parameter instead and the braces could be optional. Without braces it would be an expression, so something like this would be possible:

x = defer(foo()) gloat() fail whine()

I introduced the fail keyword to handle the rejection branch.

Seems that the @ : syntax is more readable in the expression case. I don't like that defer() ... fail ... thing I just wrote. (Not to mention the inconsistency with my original async{} block syntax.)

@getify
Copy link
Author

getify commented Dec 3, 2010

FWIW, i only chose @ because it's a free operator and not already a character (like $) which can be in identifiers. not very many operators to choose from, a lot are already taken.

but one possible (though stretching it) interpretation of @ is "execute foo(), then continue the execution queue AT bar() when ready".

@zaach
Copy link

zaach commented Dec 4, 2010

Overall, this seems like syntactic sugar for callbacks rather than promises, e.g. it does save typing and nesting for simple callback cases but doesn't let you do the interesting things promises allow any more elegantly than you could with callbacks.

Complex cases, such as the map/reduce example are just as cumbersome to implement with this syntax as with callbacks, because of the reliance on side effects and limited composability.

Consider a simpler case of waiting for two requests to complete then using the results from both. With the Q promise API, you would take what is called the intersection of two promises:

var requestA = // make request and return promise
var requestB = // make request and return promise
var intersection = Q.when(requestA, function (resultA) {
    return Q.when(requestB, function (resultB) {
        console.log(resultA, resultB);
        return resultA+resultB;
    });
});

Here intersection is also a promise, so you can use it later on to attach more callbacks to run after both requestA and requestB resolve:

Q.when(intersection, function (result) {
    // result == resultA+resultB;
});

This example is certainly possible to achieve with callbacks and @ expressions, but arguably less sexy (my attempts were, at least.)

My $0.02 :)

@getify
Copy link
Author

getify commented Dec 4, 2010

@zaach-
I was under the impression (possibly naively) that the "message passing" concept would allow what you suggest. Taking your above example, I would see it like this:

function requestA(val) {
   var p = promise;
   setTimeout(function(){
      p.fulfill(12+val);
   },2000);
   p.defer();
}
function requestB(val) {
   var p = promise;
   setTimeout(function(){
      p.fulfill(p.messages[0], 7+val);
   },2000);
   p.defer();
}
function intersection() {
   var p = promise;
   setTimeout(function(){
      var result = p.messages[0] + p.messages[1];
      console.log(result);
      p.fulfill(result);
   },3000);
   p.defer();
}

requestA(3) @ requestB(4) @ intersection(); // 15 + 11 = 26

Now, this syntax is obviously not perfect. I think there's plenty of room for brainstorming about making ways for the promises to better send messages forward in the chain.

Also, in this usage, requestA and requestB don't happen in parallel, but serially. I admit that's definitely a limitation. I can conceive of a way to still do them in parallel, but obviously the syntax gets a bit more muddled.

I think overall, I'd say I'm not necessarily trying to make a perfect and elegant syntax (although I think for simple promise/defer usage, it's pretty nice looking), but rather trying to create a native building block for handling this stuff. The more complex use-cases I can see would be simply built up by a small utility that is composed of some usage of the native @ behavior as desired.

What I want is for a language like JS that supports async and sync in the same code stream to have a more sensible way to reconcile them than the functional answer of passing around callbacks. I want the processing of promise/defer to be efficient and solid, and being native is in my mind the best way.

@getify
Copy link
Author

getify commented Dec 4, 2010

Expounding on my previous statement of using @ expressions as building blocks for more complicated behavior, you might do the above example like this:

function intersection(A, B) {
   function result() { return promise.messages[0]; }

   var p = promise, resultA, resultB;
   A() @ (resultA = result()) @ B() @ (resultB = result()) @ (function(){
      result = resultA + resultB;
      console.log(result);
      p.fulfill(result);
   });
   p.defer();
}

intersection(requestA, requestB);

By having @ expressions at our disposal, I think any more complex promise/defer functionality can be built up, hiding any of the uglier syntax. And for simple promise/defer, you can just directly use the operator and get some pretty decent looking code. No?

@samsonjs
Copy link

samsonjs commented Dec 4, 2010

I envision something that greatly improves the readability of intersection, implicit promises. Here's a better example, with a bit more going on behind the scenes but I restrict the new behaviour to var statements inside defer blocks.

defer intersection {
  var a = requestA, b = requestB // requests return promises or values
  console.log(a, b)
  return a + b
} fail (e) {
  // e is the error thrown or returned
}

// handle success alone, or success and failure
intersection.then(function(sum) { /* use sum here */ }, /* optional errback */)

// handle failure alone
intersection.fail(function(e) { /* handle failure */ })

The idea is that the result of requestA and requestB can be immediate values or promises. Assignment will implicitly have when-like behaviour. It could be rewritten to zaach's code using explicit promises, not verbatim but pretty close.

One problem with that approach is code like this:

defer {
  var sum = requestA + requestB
}

To handle that maybe you need something like @ to mark values that are deferred, so:

defer {
  var sum = @requestA + @requestB
}

Now it's getting harder to rewrite, but crippling rvalues in a deferred var seems worse. Or you just take the shotgun approach and every variable reference or function call is wrapped in an implicit when.

Prior Art

F# has a deferred binding keyword called let!. In JS we could still call it defer it might look something like this:

defer a = requestA, b = requestB
console.log(a, b)
var sum = a + b
// use the sum here

I like this less than the defer block because let in JS is quite different from let in most functional languages. A new scoped block doesn't follow the bindings so the remainder of the function must be deferred as well. It just doesn't work in JS, you need a block.

C# introduces the keyword async for use in function signatures and a corresponding await keyword that's like return but causes the function to effectively return a promise. Anything using that promise waits on it behind the scenes. At least this is my understanding of it, I could be mistaken.

The C# way with implicit promises everywhere is kind of nice but I think it's important to make async code apparent by reading it. Allowing any expression anywhere to use implicit promises is bad. It also requires much deeper changes to the language.

Any other languages w/ async support baked in that we can look at for ideas?

@kriskowal
Copy link

Here's some syntax:

when (x) { // x is bound as a promise on the outside
    // and as the resolution of that promise on the inside
} catch (reason) {
}

The when block itself is an expression which evaluates to the last evaluated value in either the when block or the catch block. This comes straight out of E.

promise!b

is analogous to Q.get(promise, 'b'), returns a promise for the property 'b' of the eventual resolution, or a promise for the property of a remote object.

promise!b()

is analogous to Q.post(promise, 'b', []), calls a function on the eventual resolution of the promise.

These kinds of operators are important for pipelining messages across long latency resolution paths.

@getify
Copy link
Author

getify commented Dec 4, 2010

For futher clarification on how I think @ helps solve more complex tasks in a graceful way, I added two examples above:

  1. click-event-handlers-as-promises
  2. intersection-serial-and-parallel

@BrendanEich
Copy link

https://gist.github.com/727232#file_ex4:click_event_handlers_as_promises

There's no savings in lines of code. Adding an operator that takes a function as right operand is not different from passing that function as an argument to the left operand via call syntax (see @FabJs). But new syntax does (a) obfuscate the function and its body's deferred code execution; (b) break compatibility with downrev browsers. What's the win besides a dedicated operator instead of ( and ) ?

https://gist.github.com/727232#file_ex5:intersection

You don't mean parallel here, as data races over closed mutable state are not tolerable (see http://weblogs.mozillazine.org/roadmap/archives/2007/02/threads_suck.html). What do you mean?

Without defining the execution model carefully, syntax fun is both endless and literally meaningless. TC39 is not inclined to add syntax that implicitly breaks the mental sequential, apparently single-threaded, run-to-completion execution model of most programmers. The "when" statement that kriskowal shows is not on the boards at http://wiki.ecmascript.org/doku.php?id=strawman:concurrency (a strawman proposal not yet ready for full consideration, Mark has said).

The objection that function syntax is too heavyweight is fair but it applies to many use-cases other than callbacks, and we're working on much lighter, but still distinctly "function here, deferred execution of body", shorthand syntax.

/be

@getify
Copy link
Author

getify commented Dec 5, 2010

@Brendan-

I was under the impression that promises/defers were not just about shortening (or cleaning up) syntax, but also about moving the location/responsibility for execution outside of a potentially untrusted function. In some of my discussions (with kris kowal and others), when the following is considered:

X(..., Y);

and relying on X to execute Y once and only once, and only at one time, is seen as a less secure paradigm. if there's a trustable traffic guard that can take X and Y and make sure that X executes (once and only once) and when it finishes (even asynchronously), then Y executes (once and only once), is a more secure paradigm. For instance:

when(X, Y)

..."when" being perhaps a trustable or neutral third party system that neither X nor Y can do anything to cause it to fail at its main purpose, to execute X and then Y, under the rules just described.

Under that same reasoning,

X() @ Y()

...keeps the responsibility of executing Y in my code, rather than trusting X (which may not be a function I wrote or control), and moreover, it entrusts that to the native built-in @ operator, which again is not a system that either X or Y can interfere with.

So, in that perspective, I'd say that X() @ Y() (or when(X,Y)) have more than just a syntactical advantage over X(...Y). Certainly, my goal with @ is not just creating a different and shorter syntax.

However, I do believe it is shorter and simpler in many of the basic sync-to-async use cases, like the event handling above. "Lines of Code" is an unfair metric. Use of the code for the common tasks is clearly shorter, cleaner, and I'd argue more semantic:

onclick(clicker) @ onclick(btn) @ function(){ ... };

vs.
onclick(clicker,function(){ onclick(btn, function(){ ...})}));

And that brings up the other point: X() @ Y() doesn't show nearly as much benefit (syntactically or functionally) as does X() @ Y() @ Z() @ W().... The fact that the operator is chainable (like the ternary operator) means it's even more expressive syntax, but more importantly there's no nesting of functions at all.

X(...,{ Y(..., {Z(..., {W(...)}}}); is not only a lot uglier, but it forces the author to either hardcode the calling of W into Z and the calling of Z into Y, or (more commonly), it forces the author to use intermediate/anonymous functions to code/express the chained calling.

Finally, to have X(...,Y) be able to propogate information (aka, a "message") from X to Y, the message has to be predefined as a parameter that Y accepts. But if Y needs other parameters, too, then you have a situation where you have to do partial currying of parameters, or other such complications. Or, you have to rely on a mutually accessible scoped variable to store the side effect in.

However, X(1,2,3) @ Y(4,5,6) has the benefit that the eventual result ("message") from the finish of X is available to Y (and only Y, in this case) without needing to effect Y's parameter-list/call signature or involve any external variable/side-effect. The "intersection" above is one possible example where that kind of "hidden side-effect" sort of thing is useful, and I'm sure others better at this stuff than me can come up with even better code snippets.

In summary, I'd say this: my ability to explain all the ins and outs of promise/defer and accurately recount the canonical examples of how/why should not be the reason that my @ idea is discounted. X() @ Y() is not just syntactically shorter than X(...,Y), it's fundmantally different in important ways that promises/defers are trying to solve.

Is @ a perfect or complete solution? Absolutely not. But it's intended to be a small native building block, instead of requiring that proper promise/defer needs an emulation lib to be loaded into every single program.

@BrendanEich
Copy link

There's no security argument here. Nothing in a browser JS embedding can promise always to run the deferred part of any form, however expressed (via special syntax or a library API such as setTimeout), because doing so enables denial of service attacks. If the user navigates away from the page, deferreds will be canceled. Same with finalizers in close methods of generators in JS1.7+, btw.

As for running once instead of twice, that's a quality of implementation guarantee. setTimeout != setInterval. If any browser had a bug where its setTimeout could fire a given timeout twice, they'd break the web (to use a popular phrase). This is not guaranteed unpossible by dedicating syntax to setTimeout or promises. Bugs happen but they can happen anywhere, and this kind of obvious bug is not an issue (I've never heard of it; easy to fix if it happened).

Yes, X({Y({Z()})}) is harder to read than X()@y()@z() but the overhead difference (two chars vs. one) goes down as you add real code.

What's more, did you check out @FabJs? It does (X)(Y)(Z), even shorter. An over-restrictive operator that requires call expression on its right (or on both sides?) is not a good use of scarce operator real estate.

You haven't defined execution model carefully, instead you've fallen into the syntax trap. Syntax is the last or at best continuous (but never at the expense of semantics) concern. First things first: is this model data-parallel or just a way of expressing JS's turn-based event loop concurrency -- with no shared mutable state -- using dedicated syntax?

/be

@getify
Copy link
Author

getify commented Dec 6, 2010

(note: i'm going to respond at length here to Brendan's most recent comments, but then, at his suggestion, I'm going to open up an es-discuss thread to further the discussion...will post a link here once it's up, and i encourage all of you to follow it and participate)

@Brendan-
Again, thanks for your comments, your time is very much appreciated. You raise many good points of discussion, so my response will be lengthy (I apologize in advance for the long-windedness).

First things first: is this model data-parallel or just a way of expressing JS's turn-based event loop concurrency -- with no shared mutable state -- using dedicated syntax?

I'm not suggesting anything (I don't think) that would require a different concurrency model from what currently exists in JavaScript.

In the same way that I call setTimeout(fn, 1000) and fn is executed 1 second from now, assuming a "free turn" is available (ie, other code isn't currently running), I'm suggesting that every operand in a @ operator expression which was a function call that deferred his own completion would be implicitly opting into the native async behavior of JavaScript. The final full completion of the expression statement (whether it has one async operand in it or 20) will ultimately be determined in pretty much the same way that a big nested tree of callbacks as parameters to nested async operations (timeouts, xhr, etc) would eventually complete.

An over-restrictive operator that requires call expression on its right (or on both sides?)

I have attempted to express in the above code examples, and in the discussion thread, that neither the lvalue nor rvalue operand of the @ operator would be required to be a function call. For instance:

(a = 5) @ (a++);

...would be perfectly legal (although silly), would be synchronous in execution/evaluation, and would be functionally identical to:

a = 5; a++; // or even:  a = 5, a++;

In fact, even function calls as operands might be still result in synchronous evaluation:

console.log("a") @ console.log("b");

...would be identical to:

console.log("a"); console.log("b");

The special async "defer the rest of the expression's evaluation for the time being" behavior of @ would only kick in (in the expression's left-to-right evaluation) if an operand was a function call AND that function (and only that function) opted into async deferral by flagging his own implicit promise as deferred (as shown in the code examples).

If a function call doesn't defer itself, or if an operand is not a function call, the @ operator would assume an implied "immediate promise fulfillment" for that operand, and just continue evaluating the expression immediately and synchronously.

If any operand of an @ operator expression does defer itself, that expression would be "suspended" and would register itself for needing normal asynchronous resolution (to complete its evaluation) just like any other JavaScript asynchronous behavior. If an @ operator expression statement is suspended, execution of the program would continue normally with the next statement in the program:

(a = 5) @ foo() @ console.log(a);
a = 10;

... if foo() defers its promise fulfillment, then the console.log() won't occur right away, as the expression statement will be suspended until foo() fulfills its promise. Program execution would continue with a = 10; and so on.

When foo() does fulfill his promise, the expression will try to resume evaluation (as its next available turn), and which point it'd move on to the console.log(a) operand. At that point, the value of a will be whatever the current scope/closure context says it is (just like with any other kind of async coding). a might be modified internally by foo() and/or it may be modified by the rest of the program before foo() completes, as shown by a = 10;

I hope I've now bettered answered your questions about the "execution model" around my idea.

There's no security argument here. Nothing in a browser JS embedding...

Agreed, the world of browser JS is far deficient in being able to talk about "secure coding patterns". Also, the word "security" is a hugely overloaded and conflated term, especially in the browser context. But afaik, the promise/defer crowd is worried about the "security" issue in a more broader sense than just a browser context -- ie, server-side JavaScript.

My goal, as I would think would be true for those entrenched in those efforts, is to explore a way to provide a unified pattern which can, at the very least, assist in such efforts, both in a server context and in a browser context (and any other JS embedding context for that matter).

My idea is to add a construct or operator natively to the language, others are working on a standardized API... but the common goal I think is to address the varied concerns of promise/defer across its different contexts in a consistent way.

Of course, the admission is that any such use in a wild-wild-west context like a browser will by its very nature be much less reliable (aka "secure") than say a server JS context. But that doesn't mean its value to the core language is any diminished. The overall desire is to be able to write code in a unified pattern/way and use that code (or most of it anyway) in both contexts, assuming of course there will be caveats that come with each context.

To illustrate that last desire more concretely, consider this: I have written a templating engine I call HandlebarJS, in JavaScript of course. It is intented to be used in both the browser and the server contexts, with the same core engine code and the same template source files. (the only different code is small adapter code for each specific context for file access -- XHR for browser, file i/o for server, etc).

Because file i/o (either browser or server) can, depending on different situations, be both sync or async, the API for my templating engine needs a clean way for the user of the API to complete a string of dependent operations, regardless of if under the covers one or all parts of the operation end up being async. The traditional approach is of course callbacks. But as noted above in the thread, representing a chain of 4 or 23 dependent (and possibly async) operations in such a system quickly becomes very awkward and brittle.

So, I currently use a chainable promises implementation I'm experimenting with. The code conceptually looks like this:

Handlebar.processTemplate(myTemplate, templateID)
.then(function(P){
   render_template(P.value); // either send the processed template markup to browser, or add to the DOM
});

Without derailing our current discussion too much with my own (obviously somewhat naive) promise experiements, every function in my API, including the above processTemplate(...) call, returns an object which can be chained off by calling .then(...), as shown. This ensures that my callback with the render_template(...) call inside it is called either immediately (if processTemplate() ended up being sync) or later (if some or all of processTemplate() ended up being async).

Granted, my promise/defer stuff here with chainable .then() is ugly, and not solving some of the core issues intended to be addressed by promise/defer implementations.

But it is doing one thing a little bit better: by virtue of the fact that multiple .then() calls can be linearly chained, I (as a user of the API) can express a simple chain of 4 or 23 (possibly async, possibly sync) operations which will cascade execution through the chain as necessary. I (as the API user) don't care (or know) what parts of the chain are subject to sync and async, I just want the whole string of operations to eventually complete, in order. And I don't want to express that with a brittle and hard-to-maintain series of 23 nested callbacks. A linear top-level chain of 23 .then() calls is much easier to deal with.

Moreover, the "message passing" from each link of the chain to the next keeps the use of the API much cleaner and doesn't rely on side-effect global variables or anything like that. The result of operation 2 (sync or async) is passed to operation 3, and so on.

The point here is that such a pattern (however flawed or limited in the greater picture of promise/defers) is a clean(er) solution than nested callbacks. And I need that same solution for both the server and the browser, so the only code that's different is the low level file i/o adapters, and all the higher-level API code is consistent and reusable.

My goal in THIS gist thread was to explore an operator or language construct which could do basically the same thing... allow the expressing of a linear chain of sync and async operations that hides from the "user" of such a mechanism as many of the ugly details of sync/async negotiation, message passing, etc, in a way that was useful for both server and browser.

Moreover, I recognize that (mostly for the server), there's a concern with promise/defer patterns that I not have to pass my function Y to function X for execution (X being a function I didn't write, don't control, and which I may not completely "trust"). In that mindset, I want to be able to execute X(), then be notified when X() completes (and the result of that execution), and then have my Y execute.

A "linear chain" of such operations is a fundmentally different paradigm than nesting callbacks, because of which code is ultimately in control of the execution/negotiation. That's what I meant by "security" earlier in this thread.

As for running once instead of twice, that's a quality of implementation guarantee

Again, the idea (on the server) is that I may not "trust" the third-party X function... we're not talking about natives like setTimeout() and if those are trust-worthy, we're talking about if I include a lib from a third-party server into my server, and use it, there's a greater "security" if there's a neutral party negotiating X and Y execution rather than me having to trust X will do the right thing with Y.

did you check out @FabJs? It does (X)(Y)(Z), even shorter

This syntax, while shorter, is still fundamentally in the callback paradigm, which is that Y is not executed by the calling code, like Y(), but is passed to some underlying system and executed for me.

I talked above in detail about the "security" argument. But let's set that aside for a moment and look at fabjs' approach in relation to parameter passing. (X)(Y)(Z) doesn't really give me a good way to directly "curry" some or all parameters to X, Y, or Z... if I have to do that, I probably have to create an inline anonymous function for each, like (function(...){...X(...);...})(function(...){...Y(...);...})(function(...){...Y(...);...});

Let me illustrate this another, more concrete way. Let's assume something like this:

function calc_max(..., callback) {
   // do some sort of ASYNC calculation of the "max" of all the `arguments` 
   // passed in, like via an Ajax call, etc.
}
function calc_min(..., callback) {
   // do some sort of ASYNC calculation of the "min" of all the `arguments` 
   // passed in, like via an Ajax call, etc.
}

// i want to calc the max of one list and the min of another list, and mutliple the two results.
// finally, i want to output that result. min/max calcs are async because of XHR, etc.

calc_max(13, 25, 7, function(res_max) {
   calc_min(14, 12, 8, function(res_min) {
      console.log("Result: "+(res_max * res_min));
   });
});

I have to hard-code my final output code into an inner-inner anonymous function callback, then hardcode that along with my parameters for my second function call to calc_min(...) inside an inner anonymous callback function that I pass to calc_max(...). Essentially, I have to program in reverse. And beyond two levels of nesting gets to be unmanageably complex.

Also, my code is mostly hard-coded now. Creating a generic, reusable multiple_min_max(...) utility that isn't hard-coded (either its numeric parameters OR what I want to eventually do with the result) is obviously possible, but the code gets a lot uglier really, really quickly.

Instead, it'd be really nice something like this were possible:

calc_max(13, 25, 7) @ 
calc_min(14, 12, 8) @ 
multiply() @ 
output()

Because then I could much easier generalize that (without all the nesting and hard-coding problems) into:

function multipy_max_min(max_list, min_list) {
   var p = promise;

   calc_max.apply(null, max_list) @
   calc_min.apply(null, min_list) @
   multiply() @ 
   function() { p.fulfill(promises.messages[0]); };

   p.defer()
}

multiply_max_min([13, 25, 7], [14, 12, 8])
@ output();

[deep breath]. OK, that was a ridiculously long response. I apologize. If you read all that, you are a super-hero.

@getify
Copy link
Author

getify commented Dec 7, 2010

Here's the follow-up blog post continuing the discussion: http://blog.getify.com/2010/12/native-javascript-sync-async/

And here's the discussion thread on "es-discuss" list: https://mail.mozilla.org/pipermail/es-discuss/2010-December/012278.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment