Closures in List Transformation #
A closure stores information. It can have access to a single parameter given to it at runtime; or to an internal data structure it can use to make decisions; or even to large chunks of your program’s state, letting you pass behavior without needing to pass information. In this post, I’ll show you how to use closures to work with arrays: in a few lines of code, you can grind a nested data structure down to just a handful of targeted values. With a few more, you can transform them into anything you need. And with a pinch of closure fairy dust, you can make those filters and transforms magical, building powerful and intelligent functionality up from a few lines of initializer code. The results will fascinate and horrify. But mostly fascinate.
This article was written in spring 2012, but every word still holds true, and it applies to JavaScript with very minimal mental translation.
Basic List Filtering #
Let’s start slow: filtering a list. Let’s start with this class, which represents an enemy in a video game.
/* figure 01 */
public class Alien extends Sprite {
public var level:int; // experience level
public var health:int; // remaining health
public var weapon:Weapon; // equipped weapon
}
Now create a bunch of aliens at random xy positions between 100 and -100. To figure out which ones are onscreen at the moment, we’ll filter out all the aliens whose x and y are less than 0; or to put it another way, select all the aliens whose x and y are greater.
/* figure 02 */
/**
* Returns a random number between abs and -abs.
*/
function randomPosition(abs:Number):Number {
return (abs * 2 * Math.random()) - abs;
}
// create 12 new aliens at random positions
var aliens:Array = [];
while (aliens.length < 12) {
var alien:Alien = new Alien();
alien.x = randomPosition(100);
alien.y = randomPosition(100);
aliens.push(alien);
}
// get only the aliens with x and y > 0
var onscreenAliens:Array = [];
for each (var alien:Alien in aliens) {
if (alien.x > 0 && alien.y > 0) {
onscreenAliens.push(alien);
}
}
// now rewrite it with Array.filter
var onscreenAliens:Array = aliens.filter(
function(s:Sprite, i:int, a:Array):Boolean {
return sprite.x > 0 && sprite.y > 0;
});
If we know we’re going to do a lot of these filtering operations, it makes sense
to define the filter function somewhere else. And since Array.filter
returns a
new Array
containing only the selected elements, we can chain the calls
together.
/* figure 03 */
/** Filter: returns only onscreen aliens. */
function onScreen(alien:Alien, i:int, a:Array):Boolean {
return alien.x > 0 && alien.y > 0;
}
/** Filter: returns only aliens wielding gauss rifles. */
function hasGaussRifle(alien:Alien, i:int, a:Array):Boolean {
return alien.weapon.type == "gaussRifle";
}
var rifleAliens:Array = aliens.filter(onScreen).filter(hasGaussRifle);
With this method, we’re getting closer to something useful and flexible; but we still need a separate function for every filtering condition, even if some of them are very simple. We need a way to parameterize our filters, and that’s where closures come in.
Smart Filters #
From here on, we’ll use the underscore.as version of filter
, which doesn’t
require that obnoxious three-argument function. It does, however, require each
chain to begin with .chain()
and end with either .value()
or .each()
, for
reasons we’ll discuss soon. It’s a tradeoff: underscore.as grants more power
than Array
has on its own, but requires the coder to invoke it. If you’re only
applying one filter, you don’t need .chain()
or .value()
.
In underscore.as, select
is a synonym for filter
. I prefer it, since I think
it better reflects what the method really does: it selects only certain elements
from the original list.
/* figure 04 */
/** Filter builder: selects within rectangle. */
function withinBounds(bounds:Rectangle):Function {
return function(alien:Alien):Boolean {
return bounds.containsPoint(new Point(alien.x, alien.y));
};
}
/** Filter builder: selects on weapon type. */
function hasWeapon(weaponType:String):Function {
return function(alien:Alien):Boolean {
return alien.weapon.type == weaponType;
};
}
/** Filter builder: selects aliens of a minimum level. */
function minLevel(level:int):Function {
return function(alien:Alien):Boolean {
return alien.level >= level;
};
}
var grenadiers:Array = _(aliens).chain()
.select(withinBounds(new Rectangle(0, 0, 800, 600))
.select(hasWeapon(Weapon.PLASMA_LAUNCHER))
.value();
var eliteRaiders:Array = _(aliens).chain()
.select(withinBounds(gameArea.getBounds(stage)))
.select(hasWeapon(Weapon.SHOCK_RIFLE))
.select(minLevel(5))
.value();
withinBounds
, hasWeapon
, and minLevel
are not filter functions; they
create filter functions on demand, each suited for a specific purpose. Combined
with the inherent chaining capability of Array.filter
, parameterized filters
make it easy to mix and match filtering operations.
Although underscore.as requires an explicit chain
call at the start of a
method chain, it also permits more operations than just Array.filter
,
Array.map
, and Array.forEach
. For example, _.pluck
returns the value of
the named property for each element in the target array. Here is a trivial
example.
/* figure 05 */
var cities:Array = [
{name: "New York", state: "NY"},
{name: "Houston", state: "TX"},
{name: "Seattle", state: "OR"}
];
var states:Array = _(cities).pluck("state");
// results in ["NY", "TX", "OR"]
And here’s an application to our alien situation.
/* figure 06 */
// filter functions for underscore.as can be simpler. One example:
/** Filter builder: selects Aliens by weapon type. */
function hasWeapon(weaponType:String):Function {
return function(alien:Alien):Boolean {
return alien.weapon.type == weaponType;
};
}
/** Filter builder: selects Weapons by percentage of ammo remaining. */
function maxAmmo(percentage:Number):Function {
return function(weapon:Weapon):Boolean {
return weapon.ammoCount / weapon.ammoMax < percentage;
};
}
// here's a sequence that uses _.pluck to go down a level midstream.
var grenadeLaunchers:Array = _(aliens).chain()
.select(withinBounds(gameArea.getBounds(stage)))
.select(hasWeapon(Weapon.PLASMA_LAUNCHER))
.pluck("weapon")
.select(maxAmmo(0.2))
.each(function(w:Weapon):void {
w.reload();
});
As you can see, _.select
and _.pluck
make it easy to extract specific values
from a multi-layered data structure and then do something to them. This style of
element selection may already be familiar to you from libraries like
jQuery.
Smarter Filters #
It’s handy to create a special-purpose filter function on the spot, but once created, that function always has the same effect. Closures can do more than that. Here’s a filter function which filters out duplicate values, by storing an array of known elements.
/* figure 07 */
import flash.utils.Dictionary;
function unique():Function {
var known:Dictionary = new Dictionary();
return function(element:*):Boolean {
if (known[element]) {
return false;
} else {
known[element] = true; // any value will do
return true;
}
};
}
var list:Array = [1, 2, 1, 2, 1, 2, 3];
var uniques:Array = _(list).unique(); // [1, 2, 3]
The unique
function defines a Dictionary
whose keys will be the objects
which have been passed into it so far. The values don’t matter: the point of the
Dictionary
class is that it can have any object as a key, not just a string.
As a pleasant side effect, we can use dictionaries to simulate the Set
class
in Java: a bag of unique values, nothing more or less. If we had stored known
values in an Array
, even detecting if an element is in the array at all
would require us to compare the target element with every element in the array,
at worst—and the worst would happen every time the uniqueness filter
encountered a brand new element. With Dictionary
, we do exactly one check for
each incoming element.
If it’s not clear how the function is storing and using the known
variable,
here’s a simpler use of the same concept:
/* figure 08 */
function buildAccumulator(startingValue:Number):Function {
var total:Number = startingValue;
return function(n:Number):Number {
total += n;
return total;
};
}
var runningTotal:Function = buildAccumulator(0);
var expenditures:Array = [
runningTotal(20),
runningTotal(24),
runningTotal(29),
runningTotal(22)
];
// expenditures is now [20, 44, 73, 95]
Each time runningTotal
is called, it adds the argument to its internal total
variable and then returns the new value of total
. The function will retain a
reference to that variable for as long as it exists.
To apply this concept to our game, let’s grab aliens with a combined experience level not to exceed 20, with individual level 3 or higher, and at least 80% health remaining.
/* figure 09 */
/**
* Filter builder: selects Aliens until their combined level equals or
* exceeds levelCap.
*/
function combinedLevel(levelCap:int):Function {
var total:int = 0;
return function(alien:Alien):Boolean {
total += alien.level;
return total <= levelCap;
};
}
// the minLevel and minHealthPercentage filters should be obvious
var squad:Array = _(aliens).chain()
.filter(minLevel(3))
.filter(minHealthPercentage(0.8))
.filter(combinedLevel(20))
.value();
Filter Creator Creators #
At the risk of sounding like the infamous Java FactoryFactory
, we’ll go one
layer deeper. We’ve been making some filter builders which are quite similar –
they just operate on different attributes. So why repeat ourselves?
/* figure 10 */
/** Filter builder builder: for a minimum numeric property. */
function minFilter(property:String):Function {
return function(minValue:Number):Function {
return function(object:*):Boolean {
return object[property] <= minValue;
};
};
}
var filters:Object = {
minLevel: minFilter("level"),
minHealth: minFilter("health")
};
For a certain type of person, a mere filter creator creator is peanuts. Why just filter our lists? Why not change them into something completely different? But personally, that’s where I draw the line; I recoil from the brink; I carefully and quietly put the laptop down, close it with a gentle click, and go out and enjoy the nice weather. Ha ha ha! Just kidding!
List Transformation #
Meet Array.map
.
/* figure 11 */
function louder(s:String, i:int, a:Array):String {
return s.toUpperCase();
}
var words:Array = ["correct", "horse", "battery", "staple"];
trace(words.map(louder).join()); // CORRECT HORSE BATTERY STAPLE
// like Array.filter, Array.map functions require three arguments
function multiplyBy(n:Number, i:int, a:Array):Function {
return function(m:Number):Number {
return n * m;
};
}
var numbers:Array = [1, 2, 3, 4];
var doubled:Array = numbers.map(multiplyBy(2)); // 2, 4, 6, 8
While filter
returns a Boolean
to narrow down an existing list, map
creates a new list, by executing a function on every element of the first list.
An example from our game:
/* figure 12 */
function bestWeapon():Function {
return function(alien:Alien):Weapon {
switch (alien.type) {
// we don't need break statements since each case returns
case "soldier": return new Weapon(Weapon.SHOCK_RIFLE);
case "sniper": return new Weapon(Weapon.GAUSS_RIFLE);
// ...other cases
}
};
}
var weapons:Array = _(aliens).map(bestWeapon);
_(aliens).chain().zip(weapons).each(function(pair:Array) {
// each pair is an [alien, weapon] array
pair[0].weapon = pair[1];
});
In underscore.as, _.zip
combines two arrays A and B into an array of pairs:
_([1, 2, 3]).zip("a", "b", "c")
results in [[1, "a"], [2, "b"], [3, "c"]]
.
Think of the values as teeth of a zipper. When you have two arrays whose
elements belong together, in the same order—like our array of aliens and our
array of weapons for them—_.zip
is the tool to use.
We call a function to get our best-weapon mapping function, because we always
want to create a new function. It’s a matter of style: would you rather remember
that some maps and filters are used with filter(myFilter)
and others are
filter(myFilter())
? Or would you rather just call them the same way all the
time? Some filters, like the unique-element filter used above, have to be
created anew each time they’re used. So for consistency, we’ll do this with all
of them.
Smart Maps and Smarter Maps #
Just like filters, we can build mapping functions to perform particular actions.
Check out how withAmmoType
creates a new array, sure, but all the elements are
the same—just with one property changed.
/* figure 13 */
function withAmmoType(ammoType:String):Function {
return function(weapon:Weapon):Weapon {
weapon.ammoType = ammoType;
return weapon;
};
}
var weapons:Array = _(aliens).chain()
.map(bestWeapon())
.map(withAmmoType(Ammo.ARMOR_PIERCING))
.value();
_(aliens).each(function(alien:Alien, index:int):Alien {
alien.weapon = weapons[index];
});
This way of assigning weapons to aliens may be easier to understand than the
_.zip
version, and it demonstrates that you can use the index argument to your
filter and map functions in underscore.as—you just aren’t required to. On the
other hand, by calling weapons[index]
, this code throws you right back into
the mindset of for (var i:int = 0; i < foo.length; i++)
… exactly the sort of
loop drudgery we want to escape. Why should we waste our time telling the
computer how to loop over a set of items? Why should we have to tell the
computer what numbers to plug into an array to get values out? With _.zip
,
we just say “transform the data in a way that’s easy to think about.”
Naturally, we won’t stop there. With our unique
function, and our squad
builders, we saw that we can build a closure which holds a running set of data.
With mapping functions, we can do considerably more.
/* figure 14 */
/**
* Map builder: matches enemy difficulty to the player characters,
* and makes sure enemy party has at least one of each type.
*/
function balance(party:Party):Function {
var targetCombinedLevel:int = party.combinedLevel + 5;
var targetAverageLevel:int = 0;
var types:Array = ["grenadier", "soldier", "sniper", "scout"];
// this time we're using all the arguments
return function(alien:Alien, i:int, a:Array):Alien {
// set target average level once we know the array length
targetAverageLevel ||= Math.round(targetCombinedLevel / a.length);
alien.level = targetAverageLevel;
// change the alien type until all required types are used
if (types.length > 0) {
alien.type = types.pop();
}
return alien;
};
}
Here we have a function which transforms an entire list of enemies to match an
in-game requirement. The possibilities don’t stop there: we could use an inner
mapping function to convert player classes to enemy types, ensuring that each
player character is confronted with an appropriate counterattack. Or we could
add a mapping which places the aliens in a formation based on the input type, by
altering their x
and y
values: .map(toFormation("phalanx"))
or something.
Next Up: Function Mutation #
In the next post, I’ll switch gears and talk about how to modify or even replace the behavior of functions at runtime. This opens up the ability to add application functionality on the fly, or just to simplify your code by doing a whole lot with just a few lines.