Fun.js – Just a sketch
In this installment I’m just going to sketch a functional JavaScript library that does not exist, but that I wish did4… 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:
- First double everything
- then take everything >5
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.
Undermine, if it did exist would need more than this, but the spirit of my desires are represented herein. ↩
4 Comments, Comment or Ping
Aleksey Kulikov
I love this article! It seems, that this approach can be perfectly implemented in component.js philosophy. Create small tool libraries to work with js in functional way. I would want to have this amazing map, reduce, filter and pipeline available.
Jun 7th, 2013
fogus
@Aleksey
The pipeline function is from underscore-contrib so you can start using it right away. The curried versions of the “big 3” are not available anywhere (someone correct me if I’m wrong), but they’re easy enough to add. The real trick is to build a library with this kind of philosophy at its core.
Jun 7th, 2013
Aleksey Kulikov
@fogus
Actually your idea about improving underscore with better currying is very close to this talk: and this guy started library with close to your name: scoreunder :)
The problem with underscore and underscore-contrib, that it’s too much monolithic, it is all in one, but often you just need a
mapor
filter.
I mentioned component.js as a good idea to create small well tested js-libraries, that do one thing well. Take a look at the 1000 existing components, but they still don’t have a good
map.
Jun 8th, 2013
Phil
function greaterThan(l, r) { return l > r; }
greaterThan(5, 100); //=> true
?
Jun 8th, 2013
Reply to “Fun.js – Just a sketch”