JS control-structure abstractions, using tailnest flattening and tailcall optimization

Claus Reinke claus.reinke at talk21.com
Thu Jul 21 07:26:16 PDT 2011


>> Tailcall optimization guarantees that the callbacks will not 
>> overflow the runtime stack, and tailnest flattening keeps the
>> level of syntactic nesting independent of the chain length.
> 
> ES.next has generators and proper tail calls already. I'm 
> hard-pressed to call the syntax extensions you propose 
> 'more readable" than what can be done with generators 
> and libraries such as taskjs (https://github.com/dherman/taskjs). 
> I have to say @< and expression-forms seem less readable in 
> general.

'more readable' wrt current function definition and call syntax,
whose problems you know very well from your own proposals.

The tailnests.html page in the repo allows you to see sugared
and desugared source side-by-side, making the readability
improvement obvious - the plain ES version only adds noise.

The current ES *syntax* is so bad that we are looking at extending
the *semantics* of ES (with generators, with block lambdas) just
to give programmers features that they should be able to define
*in ES* already (the only *necessary* semantic change are proper 
tail calls).

Generators offer a good cost-benefit ratio, and I'm not even against 
block-lambdas - they both provide useable syntax for interesting 
use cases. At least as long as the language is still awkwardly biased 
towards imperative code ("awkward" because this goes against its
prototypical/functional nature). 

But you cannot go on adding special language features for every
use case: block-lambdas have new features, but as far as control
structure abstraction go, they do no help to implement yield (I 
think?), so we add yield as well; and if (the form of) yield we add 
is not sufficient for the next use case, then we add ..? 

I'd like to get out of this loop where every interesting feature 
requires a language extension, and move on to being able to
define new language features in the language. So I think it 
would be a serious mistake to use special features as an 
argument against improving the usability of the core language.

By core language improvements, I mean function definition
and call improvements to match the tail call optimization
semantic improvement. Function definition improvements
are what I thought arrow syntax was about, but that seems
stuck, so I suggested a less ambitious alternative. Function
call improvements are addressed by a single infix application
operation (you know what I think about having to discuss
every new infix operation in the language committee).


Also, since you mention task.js: task.js uses generators, my
example showed how to implement generators - one coin,
different sides. However, while the idea of using generators 
to emulate non-preemptive threads is good, using the same
idea to handle callback chains is overkill. Writing

    spawn(function() { var result = yield operation(..); ..operations.. })

uses built-in generators as delimited continuations, just to get 
a handle on the variable binding context, when the same can be 
achieved more directly, in current ES syntax:

    operation(..).then( function(result) { .. operations.. } )

In the former variant, 'spawn' can stitch the continuations together
asynchronously, if 'operation' returns something like a promise, and
in the latter variant, '.then' can do the same, without using the built-in
generator machinery (also, generators are objects with internal state, 
not easily copied; this could be a disadvantage when trying to extend 
the control structure pattern to similar use cases).

Yes, agreed, programmers have been trained to read their
variable definitions against the left-to-right text flow, so we'll
want some additional 'let-then' sugar for the above, and then

    let result <- operation(..); ..operations..

would desugar (preferably without 'this'/'arguments') to

    operation(..).then( function(result) { .. operations.. } )

(this is also called for by Tennent's correspondence principle:
equal rights for declarative and parametric variable bindings).

But that is still just sugar, plus an interface against which the
code is desugared. By providing different implementations of
that interface (then-ables), we can reuse and augment the code,
for instance in asynchronous operations callback chains, or
with user-defined generators. The beauty of coding against an 
interface means that we could hook in different implementations.

In my example, I just wanted to make sure that this interface is
sufficient for the kind of control structure abstractions under
discussion. This highlighted three issues: abstraction over 
References is difficult in ES, abstraction just to delay evaluation 
wants very minimal syntax, and without types, implementations
will have to be selected explicitly. But the then-able interface 
itself seems to be sufficient.

Btw, I wonder what current JS engines can do with the generator-
based approach to async code: do you think it is going to
be as efficient as direct-style async code? That is, are tracing
JITs or other current optimizations good enough to eliminate
the built-in-generator overhead?

Claus
 


More information about the es-discuss mailing list