Apr 20 2009
Prototype’s Bind and Curry
I’ve been a fan of both Prototype and JQuery for a while and have no qualms recommending either. They do have somewhat different aims, and as such it’s a good idea to consider carefully which one best meets the needs of your project (or if you even need either to meet your needs). I’ll leave that discussion for another post, though.
Today, I’m gonna show you two very useful functions in prototype. Show you how to use them, and show you how they work.
First off, curry with a quick example:
function show_name(name)
{
alert(name);
}
var show_my_name = show_name.curry("Jon");
show_my_name();
The call to show_my_name will result in an alert dialog with “Jon”. The curry function allowed me to pre-load the parameters. You can also do a partial curry as follows:
function show_animal(animal_type,adjective)
{
alert("It is a " + adjective + " " + animal_type);
}
var show_dog = show_animal.curry("dog");
show_dog("fast");
show_dog("brown");
The above will result in two alert dialogs — the first saying “It is a fast dog” and the second saying “It is a brown dog”. the show_dog method only required the one parameter, because the animal type had already been pre-loaded.
Bind works in an almost identical way but with one important difference — context switching. The first parameter to bind is the desired context. For example:
var fancy_box = function(element)
{
//ensure element is a checkbox. if not, bail
if ((!(element instanceof HTMLElement)
|| !(element.nodeName.toLowerCase() === "input")
|| !(element.getAttribute("type").toLowerCase() === "checkbox"))
{
return;
}
this.isChecked = function()
{
return !!element.checked;
}
this.click_handler = function()
{
var message = this.isChecked() ? "Checked" : "Not Checked";
alert(message);
}
element.observe("click", this.click_handler.bind(this)); //observe is a prototype stand-in for addEventListener/attachEvent to allow you to ignore browser differences.
}
The element.observe line above makes use of bind to ensure the click_handler is working in the correct context. You can, however, use a different context instead.
Event handlers can be tricky and there is actually a specialized version of bind, called bindAsEventListener which works the same as bind, but also passes the event.
So…. How do they work? Well, both curry and bind (okay, and bindAsEventListener, too) rely on built in JavaScript methods call and apply.
call and apply are very similar and really only differ in one significant way. Both take the operating context as the first parameter but apply takes the arguments as an array whereas call takes the arguments as regular parameters.
To use an earlier example:
show_name.call(window,"Bob");
This will result in show_name operating in the window context and being given the parameter "Bob". This can be especially important when you use these methods on objects which don't natively support them, though.
Every function has a special variable called arguments which is like an array but isn't. It's array like behavior, though, makes the Array functions usable on it as in the following:
function say()
{
var message = Array.prototype.join.call(arguments," ");
alert(message);
}
say("Hello","Bob");
Even though the function say() doesn't explicitly take any arguments, what it really does is take a variable number of arguments, and joins them together, separated by spaces, and then alerts them. This kind of technique can be quite handy in lots of situations, but in this post, I'm concerned about how it will help us understand bind and curry.
From Prototype 1.6.1:
Object.extend(Function.prototype, (function() {
var slice = Array.prototype.slice;
function update(array, args) {
var arrayLength = array.length, length = args.length;
while (length--) array[arrayLength + length] = args[length];
return array;
}
function merge(array, args) {
array = slice.call(array, 0);
return update(array, args);
}
function bind(context) {
if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
var __method = this, args = slice.call(arguments, 1);
return function() {
var a = merge(args, arguments);
return __method.apply(context, a);
}
}
function bindAsEventListener(context) {
var __method = this, args = slice.call(arguments, 1);
return function(event) {
var a = update([event || window.event], args);
return __method.apply(context, a);
}
}
function curry() {
if (!arguments.length) return this;
var __method = this, args = slice.call(arguments, 0);
return function() {
var a = merge(args, arguments);
return __method.apply(this, a);
}
}
//...trimmed
}
First lets look at curry again.
if (!arguments.length) return this;
This just means we should bail if there are no arguments and simply return the original function.
var __method = this, args = slice.call(arguments, 0);
Two things are being done here. First, __method is storing a reference to this for later use. This allows us to make use of a closure in the following anonymous function. The second thing is a call using Array.prototype.slice (though shortened by prototype for reuse) which allows us to convert the arguments object into an array. Handy.
return function() {
Here we return an anonymous function. This is how function.curry(parameter) returns a function we can use later, for example, in an event handler or other callback situation.
var a = merge(args, arguments);
This is a fairly straightforward to read. The variable a contains a merged array of args and arguments. args is available via a technique known as closure. args, declared in the wrapping function is visible to, or within scope of, the anonymous function being returned. If you're unsure what the functions merge and update do, just know that merge simply converts the first parameter to an array and calls update. update then loops through args and appends each element to arguments (array in the update function).
return __method.apply(this, a);
Finally, our anonymous function returns the value from the original method, with the combined arguments of the original call to curry, and the arguments passed to the anonymous function when it was called.
Whew.
Still with me?
Okay, lets go.
Bind is a little more complicated, but not much, and if you understand curry, this shouldn't be much of a problem.
function bind(context) {
if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
First important difference: bind requires a context parameter. If the first argument is undefined and there is fewer than two parameters, then there really isn't anything to be done, and bind simply returns the original function.
var __method = this, args = slice.call(arguments, 1);
This is virtually identical to curry, except that instead of converting the entire arguments array, we only want the second element onwards.
return function() {
var a = merge(args, arguments);
Same as curry.
return __method.apply(context, a);
Slightly different from curry again. Instead of this, bind uses apply with a context.
For more examples on Prototype extensions to the Function prototype, see http://api.prototypejs.org/language/function.html
For those familiar with closures, you may think this is totally unnecessary -- and you may be right -- if you only need to make use of this once or twice in your web app, but if you frequently find yourself needing closures just to refer to this then maybe it's worth generalizing into a routine, or going with a framework that has it built in.
