Monday, April 7, 2008

jQuery et al Considered Harmful

Hopefully posts on lessons from Japan, thoughts on realistic collaborative computing, and costs of proxy servers later, but for now: what's the deal with the chaining so popularly touted in JS frameworks?

In 12 year old Dmitri Gaskin's Google TechTalk on jQuery, he shows the basic model:


$('DOM collection selector').
filter(fn).parents().selector('some refinement').
customEffect(params).function(fn);


Here, you build up a collection of display objects by traversing downwards using a path selector, filter out some things, then go upwards in the tree and back down again, apply some custom jQuery effects (.animate(basic,tween,parameters)), and maybe actually more, such as by applying some custom function fn to everything, and, to top it all off, allow it all to happen in any order.

Neat, right? The framework endorses a style where DOM selections are threaded between side effectors and selectors. Upon seeing this touted as a big feature, I gave it more thought than I did before: it's sugar for ";", "with", selectors, and assignment. Later in the talk, the "ends()" function was introduced, indeed highlighting the stack based style of this (mixing "with" and ";").

Let's compare a JS/XPath fragment to a jQuery fragment:

with (body.selectNodes('td/a')) {
for (var i in this) i.style.backgroundColor = '#CCC';
for (var j in this) with (j.parent)
if (this.style.height > 100)
this.move(200);
for (var k in this) k.move(300); }

with

$('td a').
css({backgroundColor: '#CCC'}).
parents().
filter(function(d){ return d.style.height > 100; }.
move(200).ends().
move(300);


The above baked-in imperative style isn't very modern. XPath is dissonant with CSS selectors more common to DOM selection, so I'll reuse jQuery there (Prototype et al work here as well). Point-free style in JavaScript is verbose (think the lambda term in map/filter/fold), so I'll reuse jQuery shortcuts. This is common too; we have a version for Flapjax extended for time-varying values to let you write:


div(
{width: $E('click').collect(add1, 0)},
'click to expand!');


So, in a more modern style, but still without the glue:


with ($('td a')) {
this.css({backgroundColor:'#CCC'});
with (this.parents())
if (this.style.height > 100)
this.move(200);
this.move(300); }


As I learned in Flapjax, using such dots is a poor man's encoding of macros (ex: we lose syntax tracking). We can pass whatever extra info around that we want as part of composition. In the case of older school non-FrTime-esque FRP, we could run exec at the end, though it is implicit in Flapjax. Thus, we're really talking about syntax and style here in terms of how to thread 'this', assuming you're ok with the problems of 'with'.

The difference between the modern JS version and the canonical jQuery version is that we use the language to show context in the former, but not in the latter. To determine the context of 'this' for any particular statement in the former, 'just' follow the with (or function) statements at the top of automatically indented blocks.

Two things make this difficult in jQuery.

  1. The ability to mix selectors and effects in a chain makes it more of a chore to pick out what is actually being selected for a particular effect. A type-based solution to providing support for this doesn't immediately jump out at me, but I suspect something using variants on returns might be doable for IDE support once JS2 arrives. More custom program analysis might be feasible, as would custom (but unsafe) baked in IDE support, but I don't see any of this happening in practice.

  2. The usage of ends makes this stack based, which introduces a cognitive load of where in the tree you are, not just what to look at from the preceding chain. The style promoted by allowing ends makes legibility go down the drain. Again, blocks, as used in the imperative form, make this explicit.

Hypocritically, you might argue, we took the dot strategy in Flapjax/FIRE!

For example, to get a count of how many bunches of rapid clicks occurred (with at least 300ms breaking apart each bunch), we would write:

var x = $E('click').calm(300).collect(add1, 0);


The difference between this sort of code, and that of above, is that we aren't bringing assignments into the picture. Something like ends or multiple effects would make no sense as we are building one value: the point is that we reuse the language. In our case, pipe and filter style of shells provide a nice explicit sugar for the threading of the stream fragment to be transformed:

var x <- $E('click') | calm 300 | collect add1 0;


Each piped expression creates a new value to be used by the next, so there is no backtracking, and to have an observable effect, you must assign the resultant value somewhere.

Further, you might argue that this already is doable in JS! In JS (and thus Flapjax), you could write:

var x, y, z;
var z = bar(z = foo(y = bar()));


But that doesn't mean you should -- I find more let* and destructuring sugar more legible due to a clear separation of left and right values. In practice, I at most see "var x = y = foo()" style usage. As noted, adding in the vagaries of assignment (point-free jQuery assignment sugar) and branching makes this even worse.

The title of this article was too strong: it is the glue promoted by jQuery style libraries that I find suspect. The dynamic nature makes IDE support difficult, and at the syntactic levels, these frameworks promote saving a few keystrokes at the expense of significant code hints in the glue layer. As APIs, they are incredibly clean -- point-free code is verbose in JS (so we promote common forms in Flapjax as well) -- and different path selection methods across browsers have different speeds, so a library to pick which is used is currently for the best. Further, language integrated querying is a big issue -- query reuse, within the client and event between clients and servers, still hasn't really been addressed yet -- so I think it's still unclear how to integrate it in the long run. However, mixing effects, selectors, and branching a la ends in the currently popular manner seems to be against the simplicity principles promoted by JS frameworks.
Post a Comment