I’ve written a book entitled Functional JavaScript due out in print sometime in June 2013. In the book I introduce and explore various techniques for writing code in a functional style using the Underscore library. As much as I would have loved to delve into the ecosystem of functional JS libraries, I didn’t want to distract from the core functional topics in the book.1 Therefore, I present a series to explore a few of my favorite functional JavaScript libraries2 focusing primarily on the feature(s) that make them interesting, innovative or unique.[^map]
In this installment I’m just going to sketch a functional JavaScript library that does not exist, but that I wish did[^maybe]… for fun I’ll call it Undermine.
I’ll start it like I start all of my JS libraries:
var exists = function(x) { return x != null; };
var truthy = function(x) {
return (x !== false) && exists(x);
};
These two functions are just meant to simplify JS truthiness. The
library itself starts with filter:
var _filter = function(fun) {
return function(coll) {
var results = [];
if (coll == null) return results;
coll.forEach(function(value, index, list) {
if (truthy(fun.call(null, value))) results.push(value);
});
return results;
};
};
function filter(fun) {
var coll = arguments[1];
if (exists(coll)) return _filter(fun)(coll);
return _filter(fun);
}
… and then map:
var _map = function(fun) {
return function(coll) {
var results = [];
if (coll == null) return results;
coll.forEach(function(value, index, list) {
results.push(fun.call(null, value));
});
return results;
};
};
function map(fun) {
var coll = arguments[1];
if (exists(coll)) return _map(fun)(coll);
return _map(fun);
}
To see map in action just run the following:
function double(n) { return 2*n; }
map(double, [1,2,3])
//=> [2, 4, 6]
But both filter and map are implemented in
a funny way. That is, they only return a result if all of the expected
arguments are given, otherwise they return a closure that expects a
collection. This is called currying. Let’s see it in action:
var doubleAll = map(double); doubleAll([1,2,3]); //=> [2, 4, 6]
Seems pretty straight-forward no? Although I curry these 2-arg functions manually, I can write a function to curry 2-arg functions automatically:
function schonfinkel(fun) {
return function(first) {
return function(last) {
return fun(first, last);
};
};
};
The schonfinkel function curries a function of two
parameters, from left to right.
Oh look! Here’s a function:
function greaterThan(l, r) {
return l > r;
}
greaterThan(500, 10);
//=> true
Here’s a curried version:
var maybeGT = schonfinkel(greaterThan);
And its usage:
maybeGT(5)
//=> function(last) { ... }
maybeGT(5)(10000000);
//=> false
Whoops. The problem is that the curried application ordering is
wrong, so let’s create a function to flip the application
order:
function flip(fun) {
return function() {
return fun.call(null, arguments[1], arguments[0]);
};
}
And so the new function becomes:
var gt = schonfinkel(flip(greaterThan));
gt(5)
//=> function(last) { ... }
gt(5)(10000000);
//=> true
gt(5)(1);
//=> false
You know what? I forgot reduce. Here it is, following a
different currying pattern:
var _reduce = function(fun, seed, coll) {
var acc = seed;
coll.forEach(function(value) {
acc = fun.call(null, acc, value);
});
return acc;
};
function reduce(fun) {
var count = arguments.length;
var seed = arguments[1];
var coll = arguments[2];
var result;
switch(count) {
case 1:
result = schonfinkel(_reduce.bind(null, fun));
break;
case 2:
result = _reduce.bind(null, fun, seed);
break;
case 3:
result = _reduce(fun, seed, coll);
break;
}
return result;
}
reduce is more complicated and inverted compared to
map and filter, but that’s only because I want
to show the use of partial application (in the
result = schonfinkel(_reduce.bind(null, fun)) snippet).
Since reduce assumes that it gets a fun then
said fun can be partially applied and the resulting
function from Function#bind Schonfinkeled… I mean curried.
Partial application is different than currying. A partially applied
function is always ready to rock, but a curried function keeps returning
closures until the last arg is given, only then is it executable.
Let’s see reduce in action:
function sum(x,y) { return x + y; }
reduce(sum, 0, [1,2,3,4,5]);
//=> 15
Seems correct no? What about a curried version:
var summate = reduce(sum, 0); summate([1,2,3,4,5]); //=> 15
So what’s the point of all this? The goal in a library like Undermine is to strive to facilitate the description of programs as a series of actions that should occur. A function that can help execute such a series is as follows:
var pipeline = function(/*, funs */){
var funs = Array.prototype.slice.call(arguments);
return function(seed) {
return reduce(function(l,r) { return r(l); },
seed,
funs);
};
};
That’s a bit subtle, but let’s see it in action:
var gimmeTheThingTHETHING = pipeline(); gimmeTheThingTHETHING(5); //=> 5
So a pipeline that’s given nothing returns a function
that just returns whatever’s given it. What about if
pipeline is given something?
var doubler = pipeline(double); doubler(5); //=> 10
So the closure returned by pipeline calls the captured
function argument in the beginning with whatever’s passed into it. What
about if I supply more functions?
var quadrupler = pipeline(double, double); quadrupler(5); //=> 20
Seems right no? I can now use pipeline to describe a
sequence of actions to take to perform some task:
var tasks = pipeline( map(double) , filter(gt(5)) ); tasks([1,2,3,4,5]); //=> [6,8,10]
The tasks are described as:
And that’s exactly what the code says! Pipelines can also be strung together:
var moreTasks = pipeline(
tasks
, reduce(function(x,y) { return x*y }, 1)
);
moreTasks([1,2,3,4,5]);
//=> 480
Pretty cool huh? That’s all for Undermine. There’s more stuff like this in my book Functional JavaScript.
Enjoy.
:F
Thanks to Michael-Keith for the idea for a better currying approach.
[^map