jQuery Rage: Pseudo-Functional Programming

Updated 2014-02-08

jQuery is the most popular javascript library out there. On top of providing DOM traversal, the library also provides various shorthands for popular actions. Its animation system was probably the best until CSS3 animations hit the stage.

On top of this, jQuery provides a lot of helper functions, that have more general purposes. In particular, it has borrowed some ideas from truly functional languages.

Here is a quick overview of concept translations, with F# as the functional language (much of the concepts are explained on MSDN):

jQuery            |  F#                       |  English
---------------------------------------------------------
$.merge(a,b)      |  a @ b                    |  append
a.is(fun)         |  List.exists fun a        |  exists
a.filter(fun)     |  List.filter fun a        |  filter
$.grep(a,fun)     |  List.filter fun a        |  filter
a.not(fun)        |  List.filter (neg fun) a  |  filter *
$.map(a,fun)      |  List.map fun a           |  map
a.map(fun)        |  List.map fun a           |  map

* neg is a function that makes the function return the opposite,
   so (neg fun) a = not (fun a)

Two of these are to be the subject of scrutiny in the following: filter and map. Here are the relevant APIs:

There are several issues with this, which I will discuss below.

 Filter/grep/has

The naming of the filtering functions is stunningly inconsistent! Where filter is the traditional, functional name, which describes the functionality best in terms of academic correctness, grep is borrowed from the Linux world, and has has appeared from thin air.

It is bewildering that filter and has do the exact same thing (except that has works looks at descendants) if supplied with a selector string (but has cannot take a function and is hence left out of the overview above).

has could easily be rewritten as a filter:

$.fn.has = function(selector) {
  this.filter(function() {
    return this.find(selector).length > 0
  }
}

filter has several ways to filter that are not supported in has. I am left confused and wondering why they did not just merge these two functions.

It might be for the love of the keyword. But has is one of the most misleading ones I have ever seen! It sounds like it is describing a capability, just like the keyword is:

car.is("red");
car.has("wheels");

These are expected return a boolean, and we certainly don’t expect them to take functions - but has doesn’t return a boolean, and it does take a function. In other words, it filters. A candidate for a better word would be Where, used in C#s own pseudo-functional language:

cars.Where(x => x.IsRed());

 Map to…?

So the two map functions seem fairly alike, right? Wrong. Here’s an example, using the first mapping function:

var values = $.map($("div"), function(val, i) {
  return val.id;
});

Here is the naïve translation to the second map function:

var values = $("div").map(function(i, val) {
  return val.id;
});

Okay, so the arguments in the callback function are swapped. This is confusing, but I’ll let that slip for now. The point is: you would expect these two functions to return the same. You would expect to get out an array of the ids of all divs. For example:

[ "div-1", "div-2", "div-3", "div-4" ]

What do you really get out? The first one gives us:

[ "div-1", "div-2", "div-3", "div-4" ]

Yay! Everything went as expected… but the second one is more problematic. It returns:

{"0":"div-1","1":"div-2","2":"div-3","3":"div-4","length":3,"prevObject":{"0":{},"1":{},"2":{},"length":3,"prevObject":{"0":{"location":{}},"context":{"location":{}},"length":1},"context":{"location":{}},"selector":"div"},"context":{"location":{}}}

You might recognize this as a jQuery object. That’s right. Mapping from a jQuery object to its ids returns a jQuery object describing … ids. Wat.

This seems blatantly stupid, as jQuery objects are defined, in jQuery, as describing DOM elements. Not text strings, not numbers. DOM elements. The only sensical thing for a map function to return would be an array.

I like that jQuery does some unboxing for us, turning the input jQuery element into an array, but it creates confusion that it automatically boxes them again before outputting, especially as the first mapping function doesn’t do this. There is a disconnect in behaviour between to functions of the same name.

We can get the elements we want by manually unboxing the values returned from the second mapping function:

values = values.get();

.get is another jQuery function, which, according to documentation, “grants us access to the DOM nodes underlying each jQuery object”. But unlike what the documentation says, in our case, we don’t get DOM nodes - we get strings.

To make matters worse, another function with the same name exists: $.get. These, however, are entirely unrelated. $.get makes an asynchronous GET request - and has nothing to do with unboxing!

values.get() could actually be replaced with values.toArray(). This function has the same problem with its documentation referring to DOM elements as the other, but its name is better. The fact that a better function exists begs the question: why does .get exist?

 Conclusion

jQuery is great because it makes a lot of things easy! On top of that, it has a very active community, so it is not a stale language, and it is easy for beginners to get help and get started.

However, jQuery does not do everything equally well. The above has been a collection of items I think they do badly, with regards to their pseudo-functional programming features.

Functional languages are powerful, and adding pseudo-functional features to a language is far from a bad idea (Microsoft does the same with C#).

The language as it is now, however, is highly inconsistent and counter-intutive. Basic features with arcane workings are a hinderance to learning. The problem is that a lot of applications already use these functions as-is, bad naming, signatures and all.

jQuery just went into version 2.x, dropping support for older browsers. This is in accordance with Semantic Versioning 2.0.0, as they break backwards compatibility. I find it sad, however, that they did not grasp the opportunity to revise some language inconsistences (like what Python3 did).


 Updates

2014-02-08 I opened a ticket regarding the documentation of the .map function, and it has probably been fixed now. However the API is still counter-intuitive.

 
24
Kudos
 
24
Kudos

Now read this

Web apps: validate, delegate, respond

Web apps should be a thin, easily replacable front to your business logic. It should be modular and composable. I have written about this before. But how do you — in practice — decouple the business logic from the web frontend? I like to... Continue →