The Bungee Blog

News, updates and rants around The Bungee Book (the landmark book on Prototype and script.aculo.us)

Archive for February, 2008

Neuron Workout Solutions #2

It is high time for this second installment of Neuron Workout Solutions™, a series that answers the questions and challenges at the end of many chapters in the book. Today we’ll address the challenges closing chapter 5, which focuses on the wonderful Enumerable mixin.

Read more

3 comments

Mixing argumentNames and wrap for varargs-like sweetness

Back in the first Neuron Workout solutions, I suggested an exercise for implementing varargs-like functionality using two of Prototype’s extensions to Function: argumentNames and wrap. I also promised I would post a possible solution here.

The idea was to provide a mechanism that would take an object or class in, and alter any method ending with a parameter named anythingElse so that such methods would end up with any “remaining” arguments stored as an array in this special parameter.

As promised, here’s the code for it:

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Object.enableVarArgs = function(object) {
  for (var methodName in object) {
    var method = object[methodName];
    if (!Object.isFunction(method)) continue;
    (function() {
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;
      object[methodName] = method.wrap(function() {
        var args = $A(arguments), proceed = args.shift();
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;
        return proceed.apply(this, args);
      });
    })();
  }
};
 
Class.enableVarArgs = function(klass) {
  return Object.enableVarArgs(klass.prototype);
};

Before diving too much into the inner workings of this, let’s put together a demo object, reuse it for a demo class, and see whether this all works:

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
var obj = {
  method1: function(a, b) {
    console.log("CALLED: method1(" +
      Object.inspect(a) + ', ' +
      Object.inspect(b) + ")");
  },
 
  someMethod: function(a, b, anythingElse) {
    console.log("CALLED: someMethod(" +
      Object.inspect(a) + ", " + Object.inspect(b) + ", " +
      Object.inspect(anythingElse) + ")");
  },
 
  method2: function(anythingElse) {
    console.log("CALLED: method2(" +
      Object.inspect(anythingElse) + ")");
  }
};
 
var Klass = Class.create(obj);
var obj2 = new Klass();
 
Class.enableVarArgs(Klass);
Object.enableVarArgs(obj);
 
obj.method1(1, 2, 3, 4);
obj.someMethod(1, 2, 3, 4);
obj.method2(1, 2, 3, 4);
obj2.method1(1, 2, 3, 4);
obj2.someMethod(1, 2, 3, 4);
obj2.method2(1, 2, 3, 4);

I used Firebug’s console object here, but you may use interactive alert calls just as well. On my browser, the output goes like this:

CALLED: method1(1, 2)
CALLED: someMethod(1, 2, [3, 4])
CALLED: method2([1, 2, 3, 4])
CALLED: method1(1, 2)
CALLED: someMethod(1, 2, [3, 4])
CALLED: method2([1, 2, 3, 4])

Okay, so how does this work?

Wel, we start by iterating over the object’s methods. This can easily be done by using a regular for…in loop to grab all the properties and filter using Prototype’s Object.isFunction predicate (which is just a typeof test).

11
12
13
  for (var methodName in object) {
    var method = object[methodName];
    if (!Object.isFunction(method)) continue;

Now the major trick was to avoid scope/reference issues with the remainding variables, most importantly argNames.

Because of JavaScript scoping issues, argNames will usually be a shared reference for all iterations of that loop. For instance, consider our test object: two of its methods match our criteria: someMethod, with argument names a, b and anythingElse, and method2, with just anythingElse. Because of this, the method replacement code (the one used in our wrap call) for someMethod will end up using the value of argNames that was set by processing method2, resulting in a wrecked final arguments list.

The traditional way of coping with such situations is to give such variables a guaranteed private scope, which is achieved by creating an anonymous function for the scope and calling it right after it’s defined (a technique used for implementing the famous module pattern, too).

This is why we wrap the remainder of our code inside this anonymous function, which we then call immediately (hence the weird )() code fragment).

14
15
16
17
18
19
20
21
22
23
    (function() {
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;
      object[methodName] = method.wrap(function() {
        var args = $A(arguments), proceed = args.shift();
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;
        return proceed.apply(this, args);
      });
    })();

So on to the code inside this anonymous function.

We start by filtering more on the last argument name, to check whether it is indeed anythingElse. As we’re not in the lexical scope of the surrounding for loop now, we can’t just call continue, we need to return from the anonymous function (since the loop has no code after this function, it’s pretty much equivalent).

15
16
      var argNames = method.argumentNames();
      if (argNames.last() != 'anythingElse') return;

Then we replace the former code for the method by our new code, which wraps the original one. Wrapped methods always received the former version as their first argument, which we traditionally call proceed. To easily tweak the arguments actually passed to our new methods, we grab the JavaScript predefined arguments variable, pass it to $A to make sure we have a proper, full-fledged Array to play with, and take the first argument out as our original method (proceed).

18
        var args = $A(arguments), proceed = args.shift();

All that is left to do is put all arguments from the anythingElse-positioned one to the end of the actual argument list in an array to be passed instead of anythingElse, and “cut” the arguments array to the proper length:

19
20
        args[argNames.length - 1] = args.slice(argNames.length - 1);
        args.length = argNames.length;

The actual call is performed by the native apply method on our original function, which has the good sense of taking the arguments as an array (which makes it ideal for our purposes). Of course, if there was an original returned value, we need to propagate it by returning it too.

20
        return proceed.apply(this, args);

Feel free to ask your questions in the comments, and discuss your own solutions if you had worked out any!

You can also download the full demo script for this example and play with it (you’ll need prototype.js loaded, obviously).

Also note I’m working on the second installment of the Neuron Workout Solutions, so stay tuned!

No comments

No more excuse not to upgrade to 1.6!

I finally find a moment to spread the good news: Tobie equipped Prototype with a deprecation/removal assistance layer. This extra deprecation.js file, to be loaded right after Prototype, spews clear, detailed, advise-replete messages in your Firebug console whenever your scripts use a now-removed or deprecated feature from earlier versions of the lib.

It’s a great help for migrating, and migrate you should. Get the whole details, usage recommendations, and grab the file on the relevant post at the Prototype Blog.

No comments

Podcast at Prag’s

If you’d like to hear me talk about why raw JavaScript and raw DOM interfaces are not enough, and how Prototype and script.aculo.us make it all nicer to play with, check out the fresh podcast at Pragmatic Programmers.

Sorry for the average audio quality on my side of the interview: it was done through Skype…

Let me know what you think!

No comments