Dear all,
we have seen examples of how to define control-structure
abstractions via block-lambdas (and Smalltalk blocks), including
non-local returns out of user-defined loops.
I'd like to provide an example of how to do something like this
with JS, using only the proposed syntactic sugar for flat syntactic
tailnests and the upcoming support for flat runtime tailcall stacks
(for callback chains, the two complement each other).
Building up from small structures, the example aims to implement
a variant of "yield". With conventional blocks, this requires the
upcoming generators language extension. Block-lambdas do
not help here, I think, because yield can suspend and resume at
statement level, and even the slightly unusual sequel feature
does only support non-local return, no resume (calling a sequel
returns to a lexically scoped elsewhere, not to the caller).
If JS had call/cc or delimited continuations, we could implement
yield on top of that, but that isn't likely. If we were to rewrite our
code in continuation passing style (cps), we could implement
call/cc, but even in languages with lightweight function syntax,
that is not considered very readable. There are modular variants
of continuation passing style that can be made to look very similar
to "normal" code, but these become unreadable in JS syntax.
Cps without tailcall optimization also quickly overflows the stack.
Which is where tailnests (readability) and tailcalls (efficiency/
useability) come in, addressing the issues that cps in current
JS leads to overflows in syntactic and runtime stack nesting.
First, the programming pattern: in plain cps, we'd have nested
callback chains feeding intermediate results into the rest of the
callback chain (assuming currying for the callback parameter):
operation(..)( function(result) { ..operations..} )
With paren-free right-associative calls (f @< arg) and brace-free
definitions (function(arg) => expr) for expression functions, this
becomes:
operation(..) @< function(result) => ..operations..
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.
In modular cps, instead of every operation taking a callback
parameter, every operation returns its result in an object
implementing a single method 'then' (the ideas are standard,
the translation to JS is not). Calling 'then' with a callback feeds
a value/intermediate result to the callback.
operation(..).then @< function(result) => ..operations..
(which can be read as a clumsy way to write
"let result <- operation in operations", but the ".then" allows
us to give different meanings to the variable binding, eg., it
could mean "foreach result from operation, do operations", or
it could store the continuation and jump elsewhere, as yield)
The most basic operation is 'value', which just wraps a value
into a "then-able":
function value(val) => { then : function(cont) => cont(val) }
Since statements represented as then-ables are objects,
conditional statements are just conditional expressions:
cond ? t : e
or, if the condition involves then-able statements, too:
cond.then @< function( c ) => c ? t : e
which we can either use directly, or wrap in a function (the
then/else-branch arguments are themselves wrapped to
delay their evaluation):
function if_(cond) => function(ft) => function(fe) =>
cond.then @< function( c ) => c ? ft() : fe()
Hence, a while-loop with then-able condition and delayed body:
function while_(cond) => function(body) =>
if_(cond)
(function() => body().then @< function() =>
while_(cond)(body) )
(function() => value( null ) )
You can find these examples online [1], so to limit this email,
I'll jump right to the interesting bit, which is implementing yield:
"yield_(val)" produces a then-able that behaves a bit unusually:
instead of passing a value to continuations passed via ".then()",
it swallows all such continuations, storing them for later use
(in plain cps, we'd have the whole continuation at hand; in this
modular cps variation, we have to collect nested continuations).
To get at the yielded value, we can call "yield_(val).value(cont)",
and to restart the "generator", we can call "yield_(val).next()",
which applies the internally stored continuations to "val". In code:
function yield_(val) => { then: stack(val) };
function stack(val) => function(c1) =>
{ then: function(c2) => stack(val)(comp_then(c2,c1))
, value: function(cont) => cont(val)
, next: function() => c1(val) };
function comp_then(c2,c1) => function(val) => c1(val).then(c2);
Again, the commented code and a couple of example generators
can be found online [1]. If you use the desugaring page, you can
see why this style isn't popular without syntactic sugar. But the
functionality is plain JS, no semantic changes!
The code could be made more readable by providing
"let <-"-desugaring to then-ables, but JS is different enough from
other languages providing similar features that I want to make
sure that this pattern is the one to aim for before proposing that.
The code could be shortened further by combining arrow-syntax
with "@<", instead of the less ambitious expression functions of
the tailnests proposal. And extended object-literals would shorten
inline then-ables.
Claus
[1] https://github.com/clausreinke/jstr/tree/master/es-discuss
Examples are in then.js, but if you clone the repo, you can
load them via tailnests.html, desugar into plain JS via jstr,
and evaluate the resulting code. So you can play with it!-)
_______________________________________________
es-discuss mailing list
es-discuss@mozilla.org
https://mail.mozilla.org/listinfo/es-discuss