Curry is one of my favorite functions - it’s one of those indispensable tools that you never know about until you need it, and then in a flash you realize all of the potential applications for.
First of all, I need to give credit where credit is due. I learned about curry() while using Dojo, and was shocked to find out that MooTools’ pass() didn’t do the same thing. So special thanks to Alex Russell of Dojo and the other dude whose code I lifted and adapted for use in MooTools.
What is a Curry?
Aside from being a delicious spicy Thai mainstay, it’s also the process of wrapping a function inside another function, such that some of the inner functions’ arguments have already been defined.
Say What?
Let’s say you have a function printName, which expects lastName and firstName as arguments. To whit:
printName = function(lastName, firstName){
alert("Hello, " + firstName + " " + lastName);
}
Now let’s say you have a list of names that are in a Hash, like this:
names = $H({
'Jones': ['Barnaby','Sally','Timmy','Bridget'],
'Smith': ['Gabby', 'Kelly', 'Crabby']
});
Here’s how a curry can help:
names.each( function(firsts, lastName){
var fn = printName.curry(lastName);
firsts.each( fn );
})
Whoa.
What’s happened above is that we’ve walked through the last names object, which gives us a list of the first names (’firsts’) and a string key which is the last name (’lastName’). Next, we create a temporary function ‘fn’ that is a copy of printName, but with the first argument (lastName) already filled in. Next, we pass each of the first names in turn to the temporary function. Since the first argument that each() passes is the value of the array item (the first name), we are effectively calling printName with both the first argument (from the curry) and the second argument (from each()) filled out.
In this case, curry() helped us to avoid a potentially inelegant series of for() loops. We could even state the same code in a simpler (albeit more opaque) manner:
names.each( function(firsts, lastName){
firsts.each( printName.curry(lastName) );
});
How does it work?
Curry() takes a function’s argument list and holds it in a closure, which it returns. The big difference between my MooTools curry() and MooTools’ own pass() is support for multiple levels of curry. Once you call pass(), calling pass() again on the result won’t get you anywhere. My curry() can attach as many arguments as you want. Another difference is that pass() takes an array as it’s first argument if you want to pass multiple arguments. For curry(), just call it as if it were the target function and the args will be converted to an array for you.
Function.prototype.curry = function()
{
var method = this, args = Array.prototype.slice.call(arguments);
return function()
{
return method.apply(this, args.concat(Array.prototype.slice.call(arguments)));
};
};
Tags: abstract, advanced, mootools
In our increasingly Ajax-centered world, where things happen in an unpredictable order, it is often very important to make sure some list of things has occurred prior to taking an action. This can be especially painful, for example, when multiple XHR events are pending and a final action relies on them all being completed in no particular order.
Presenting Aggregator, a deceptively simple little widget with a lot of firepower. The concept is as follows:
The Aggregator is called like this:
Collector = Aggregator( {stages: ['first', 'second']}, alert.pass('all done'));
Collector is a closure that provides the aggregating functionality. You use Collector like this:
Collector( 'first' );
This toggles the first stage switch and runs the latch. If the latch returns true, the destiny function is called. In this case, since we didn’t specify a latch, the default is being used. Therefore, the destiny function will not yet call because ’second’ has not yet been satisfied.
Asset.javascript( 'somescript.js', {onload: Collector.pass('second') });
You can cheat and run the second step below.
Collector('second');
When the javascript file is loaded, the Collector function will be called with ’second’ as it’s argument. When that happens, the latch will be satisfied and the destiny function alert(’all done’) will be called.
The Aggregator can also collect data. A good example is when using Google Maps API. Let’s say you want to wait until an address has been geocoded, an image has been loaded, some data has been loaded, and a few other operations are done before calling an “addMarker” function:
var gc = new API.ClientGeocoder;
var markerBuilder = Aggregator({
stages: ['placemarks', 'markeropts', 'markerdata', 'nodata', 'image'],
latch: function(s){
with (s)
return placemarks && markeropts && image && (markerdata || nodata);
}
}, addMarker);
/* Step 1: Start Geocode */
gc.getLatLng(address, markerBuilder.curry('placemarks'));
/* Step 2: Fetch Marker */
var markerOpts = {
clickable: true,
draggable: (opts ? (opts.canmove ? true : false) : false),
minZoom: (opts ? (opts.weight ? opts.weight : 1) : 1)
};
markerBuilder('markeropts', markerOpts);
/* Step 3: Fetch Category Data */
if (categoryId) {
// hypothetical data loader "DataAdapter"
var MarkerData = new DataAdapter('Marker', categoryId);
MarkerData.get(markerBuilder.curry('markerdata'));
}
else {
markerBuilder('nodata');
}
/* Step 4: Image */
var Image = new Asset.image('/someimage.jpg', {onload: markerBuilder.curry('image',Image)})
The appeal of this approach is immediately obvious to anyone who regularly needs to dispatch multiple concurrent operations using XHR or similar asynchronous processes.
The full code for Aggregator is very small, as you can see:
Aggregator = function(config, finalfn)
{
var latch = null; var priv = {}; var state = $H({});
// first, assign the latching semantics.
// either use the configured latch function or a simple one.
// the simple latch below examines each member of 'state' to see if it
// is defined. If not, it quits. If the latch gets all the way through,
// it succeeds.
latch = config.latch ? config.latch : ( function(state,priv){
// release when all values are defined.
return ! (state.contains(false));
});
// next, set up the private space for stages and the latch state for each.
$each(config.stages || config, function(v){
state[v] = false;
priv[v] = '';
});
// next, define the closure.
var r = function(stage, data){
if (stage && !state[stage]) {
priv[stage] = data;
state[stage] = true;
}
if(latch(state,priv))
finalfn(priv);
}
return r;
}
Note that if you want to pass data values to an Aggregator lambda-function, you also need my Function.curry definition, which allows you to build functions with a built-in argument list. Curry is posted on the Nibbles and Bits blog as well.