[this has grown a bit long, because in addition to concrete
suggestions, it discusses the design problems leading up
to these suggestions and includes an extended example;
I hope that makes for an easier read overall, but you
might want to allocate a few minutes to read it in one go]

Summary:

   Callback nesting is an important programming pattern,
   not just in asynchronous programming. The pattern is
   just barely useable in current Javascript (so bad that most
   attempts to address the problem try to avoid the pattern,
   making do with non-nested callbacks or resorting to
   pre-processors that suggest language extensions going
   beyond mere syntax).

   Now, Javascript can handle callback nesting just fine[*],
   semantically, it is only the syntax that gets in the way,
   and it is those syntax issues that are addressed here.

Context:

Parens and braces indicate grouping. If the intended grouping
is unambiguous, it should be possible to omit the redundant
parens/braces, to avoid distracting syntax noise. This has been
discussed for Javascript's control structures [0], but there is
another source of nested syntax that has been driving coders
up the walls[1]: the nesting that follows function definitions
and applications.

Problems:

1 Javascript's grammar requires parens around arguments
   in function definitions and applications, and braces around
   function bodies (expression closures experiment with skipping
   the latter [2,3])

2 Javascript function definitions do not determine their
   number of arguments (length is a "typical" number, 15.3.5.1,
   but that is an approximation, and functions can be called with
   fewer or with more arguments)

3 The number of function arguments is fixed (independently)
   at each call site, by the placement of parens (making it more
   difficult to remove those parens).

We can't just relax the grammar to solve (1), because the
parens serve a purpose separate from grouping (3), and that
has its cause deep in the language definition (2). With lots of
code relying on optional arguments with default values or
using arguments as unbounded lists, eliminating (2) sounds
unlikely (Harmony's rest parameters and parameter default
values not withstanding).

A general solution to excessive parens and braces surrounding
function definitions and calls remains elusive, but tail nests turn
out to be a useful special case: they characterize many of the
frequent problem cases (such as callbacks as final arguments
in asynchronous APIs [1]) and their tail position provides
additional leverage against the arity issue noted above (3).

Definition

   Tail nests are to syntactic nesting depth as
   tail calls are to runtime call stack depth.

   In terms of parens and braces, that is code somewhat like

       (... ( .. ( .. ))) or { .. { .. { .. }}}

   or mixtures of these (possibly with semicolons thrown in).

The idea for reducing nesting depth in the context of tail nests
translates directly from tail call optimization:

   - if the start of a nest can be determined by other means
       (redundant start marker)
   - and the end of that nest coincides with the end of the
       enclosing nest (tail nest; implies redundant end marker)

   + then the wrapping of that nest is redundant and can
       be omitted (the tail nest need not add its own parens/
       braces, keeping the nesting depth constant no matter
       how many tail nests are added)

The question is:

Which tail nest can we identify in real-world Javascript code,
and can we find a way to use the redundancy, so that we can
remove some parens/braces without confusing things?

Running example and suggestions:

My prime example would be callback nesting. Here is some
code straight from an appropriately named nodejs group
thread["I love async, but I can't code like this" 1]:

mainWindow.menu("File", function(err, file) {
 if(err) throw err;
 file.openMenu(function(err, menu) {
   if(err) throw err;
   menu.item("Open", function(err, item) {
     if(err) throw err;
     item.click(function(err) {
       if(err) throw err;
       mainWindow.getChild(type('Window'), function(err, dialog) {
         if(err) throw err;
         ...
       });
     });
   });
 });
});

Getting rid of the template error handling is straightforward,
so I'll ignore that for the rest of this discussion, just keeping
the syntactically problematic part of the code structure:

mainWindow.menu("File", function(file) {
 file.openMenu(function(menu) {
   menu.item("Open", function(item) {
     item.click(function() {
       mainWindow.getChild(type('Window'), function(dialog) {
         ...
       });
     });
   });
 });
});

which leaves us with the problem of closing all those tail nests
(the semicolons before '}' could be supplied by ASI, I assume,
but we need to track what happens once we remove braces,
so I leave them in for now).

The start of a function body is redundantly marked by the end
of its formal parameter list, and the end of the callback bodies
in tail nests like these is redundantly marked by the end of
their enclosing calls' actual parameter lists. This redundancy
motivates the first suggestion:

Suggestion 1: make braces surrounding function bodies in
   function definitions optional

   (function bodies without explicit braces extend as far as
   possible, but their extent can be limited from the outside)

Something similar was suggested for expression closures[2,3],
replacing inner braces by outer parens (is that part included
in #-functions?). The example changes as follows:

mainWindow.menu("File", function(file)
 file.openMenu(function(menu)
   menu.item("Open", function(item)
     item.click(function()
       mainWindow.getChild(type('Window'), function(dialog)
         ...
       );
     );
   );
 );
);

Since {}-blocks can now be implicit, we should inform ASI.

Suggestion 1a: augment ASI to treat an implicit block ending
   the same way as an explicit closing brace '}' (insert missing
   semicolon at block end)

That means we can still drop most of those semicolons:

mainWindow.menu("File", function(file)
 file.openMenu(function(menu)
   menu.item("Open", function(item)
     item.click(function()
       mainWindow.getChild(type('Window'), function(dialog)
         ...
       )
     )
   )
 )
);

Now for the parens that specify argument lists of function
calls. We can't just remove them, because they carry
information about how many arguments to pass (neither
passing the arguments one-by-one nor passing too many
arguments at once is equivalent, thanks to problem (2)).

But perhaps we can use the same trick we used for function
definitions in suggestion 1: let the context tell us where the
function application ends. For instance, if we have

   (f(x,z,y))

then the inner parens are redundant, as long as we assume
that a function application without explicit parens extends
as far to the right as possible

Suggestion 2': [to avoid ambiguities, this will be refined below]
   make parens surrounding argument lists of function
   applications optional

   (function applications without explicit parens extend as far
   as possible, but their extent can be limited from the outside)

Using this, we can rewrite our example code by moving the
function application parens one level out, but..

mainWindow.menu("File", function(file)
 (file.openMenu function(menu)
   (menu.item "Open", function(item)
     (item.click function()
       (mainWindow.getChild type('Window'), function(dialog)
         ...
       )
     )
   )
 )
);

"Argh!", that didn't really help, did it?-) Well, actually it did,
but the syntax is a little broken, so the improvement isn't
obvious.

For the moment, we remove the ambiguities in the simplest
way: assume a new infix operator '@' (for application, [@]),
whose only purpose is to help us (parsers) to identify
function applications in spite of optional parens:

   '(f @ a1, .. , an)' is equivalent to 'f(a1,..,an)'

   [in the former, the parens are optional, delimiting the
    application from the outside; in the latter, the parens
    are part of the language construct]

Parser experts might be able to come up with something
nicer, the idea here is simply that 'f @ ' starts a function
application the same way that 'f(' does, but without the
expectation of a closing ')' to match.

Suggestion 2:
   make parens surrounding argument lists of function
   applications optional

   (function applications without explicit parens extend as far
   as possible, but their extent can be limited from the outside)

   To avoid ambiguities, function applications with optional
   parens retain an explicit start marker for their argument
   list: '@' instead of '(', with no matching end marker.

   '(f @ a1, .. , an)' is equivalent to 'f(a1,..,an)'

   [in the former, the parens are optional, supplied by the
    usage context; in the latter, the parens are a required
    part of the language construct]

Our example code, with this suggestion:

mainWindow.menu("File", function(file)
 (file.openMenu @ function(menu)
   (menu.item @ "Open", function(item)
     (item.click @ function()
       (mainWindow.getChild @ type('Window'), function(dialog)
         ...
       )
     )
   )
 )
);

Now, the reason that this is any better, in spite of the same
number of parens, is that the parens are one level up, where
they can be made redundant by other nesting constructs at
that level (remember the tail call analogy: we don't avoid
nesting entirely, we just reuse outer nests, to keep the level
of nestings constant).

To begin with, the 'mainWindow.getChild' call ends where
the 'item.click' callback ends

mainWindow.menu("File", function(file)
 (file.openMenu @ function(menu)
   (menu.item @ "Open", function(item)
     (item.click @ function()
       mainWindow.getChild @ type('Window'), function(dialog)
         ...
     )
   )
 )
);

The 'item.click' call ends where the 'menu.item' callback ends:

mainWindow.menu("File", function(file)
 (file.openMenu @ function(menu)
   (menu.item @ "Open", function(item)
     item.click @ function()
       mainWindow.getChild @ type('Window'), function(dialog)
         ...
   )
 )
);

and so on, until we are left with:

mainWindow.menu("File", function(file)
 file.openMenu @ function(menu)
   menu.item @ "Open", function(item)
     item.click @ function()
       mainWindow.getChild @ type('Window'), function(dialog)
         ...
);

Which is means that we have managed to get rid of
that tail nest of parens and braces, without any semantics/
runtime changes, just by changing the syntax. In theory,
at least!-)

In practice, I'd be very interested to hear whether
these suggestions (or more suitable variants) would
be implementable in your Javascript frontends. If we
could get rid of the '@' and commas as well, that would
be even better, but that appears to conflict with curried
function application.

Claus

PS. In the running example, callbacks indicated asynchronous
   code, and there have been lots of attacks on that problem,
   ranging from libraries over preprocessors to language
   extensions. But callback nesting is typical of a wider range
   of programming patterns, to which most of the async-
   specific proposals do not apply. In contrast, the suggestions
   here address some async-independent and purely syntactic
   shortcomings in Javascript's core functionality.

[*] ok, tail call optimization would help, but that seems to be
   on the way in?
[@] '@' is traditional for function application. In Javascript,
   it seems to be used only for conditional compilation in
   JScript, where its use seems limited to prefix namespacing
   (also recommended only in comments)

[0] http://brendaneich.com/2010/11/paren-free/
[1] "I love async, but I can't code like this"
http://groups.google.com/group/nodejs/browse_thread/thread/c334947643c80968/e216892d168a7584
[2]
https://developer.mozilla.org/en/New_in_JavaScript_1.8#Expression_closures_(Merge_into_own_page.2fsection)
[3] http://perfectionkills.com/a-closer-look-at-expression-closures/



_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss

Reply via email to