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