(This post is Part I of II. The recipes in this post are excerpted the book JavaScript Allongé.)
combinators
The word “combinator” has a precise technical meaning in mathematics:
“A combinator is a higher-order function that uses only function application and earlier defined combinators to define a result from its arguments.”–Wikipedia
In this essay, we will be using a much looser definition of “combinator:” Pure functions that act on other functions to produce functions. If Objects are nouns and Methods are verbs, Combinators are the adverbs of programming.
If we were learning Combinatorial Logic, we’d start with the most basic combinators like S
, K
, and I
, and work up from there to practical combinators. We’d learn that the fundamental combinators are named after birds following the example of Raymond Smullyan’s famous book To Mock a Mockingbird.
There are an infinite number of combinators, but in this article we will focus on combinators that are useful when working with Plain Old JavaScript Objects (“POJOs”) and with instances.
Splat
In recent versions of JavaScript, arrays have a .map
method. Map takes a function as an argument, and applies it to each of the elements of the array, then returns the results in another array. For example:
[1, 2, 3, 4, 5].map(function (n) {
return n*n
})
//=> [1, 4, 9, 16, 25]
We say that .map
maps its arguments over the receiver array’s elements. Or if you prefer, that it defines a mapping between its receiver and its result. Libraries like Underscore provide a map function. It usually works like this:
_.map([1, 2, 3, 4, 5], function (n) {
return n*n
})
//=> [1, 4, 9, 16, 25]
Why provide a map function? well, JavaScript is an evolving language, and when you’re writing code that runs in a web browser, you may want to support browsers using older versions of JavaScript that didn’t provide the
.map
function. One way to do that is to “shim” the map method into the Array class, the other way is to use a map function. Most library implementations of map will default to the.map
method if its available.
This recipe isn’t for map
: It’s for splat
, a function that wraps around map
and turns any other function into a mapping. In concept, splat
is very simple:
function splat (fn) {
return function (list) {
return Array.prototype.map.call(list, fn)
}
}
Here’s the above code written using splat
:
var squareMap = splat(function (n) {
return n*n
});
squareMap([1, 2, 3, 4, 5])
//=> [1, 4, 9, 16, 25]
If we didn’t use splat
, we’d have written something like this
var squareMap = function (array) {
return _.map(array, function (n) {
return n*n
})
};
Functional programming wonks will explain that something called partial functional application would be handy here. If JavaScript had it. Which it doesn’t. Oh well.
And we’d do that every time we wanted to construct a method that maps an array to some result. splat
is a very convenient abstraction for a very common pattern.
(splat
was suggested by ludicast)
Get
get
is a very simple function. It takes the name of a property and returns a function that gets that property from an object:
function get (attr) {
return function (object) { return object[attr]; }
}
You can use it like this:
var inventory = {
apples: 0,
oranges 144,
eggs: 36
};
get('oranges')(inventory)
//=> 144
This isn’t much of a recipe yet. But let’s combine it with splat
:
var inventories = [
{ apples: 0, oranges: 144, eggs: 36 },
{ apples: 240, oranges: 54, eggs: 12 },
{ apples: 24, oranges: 12, eggs: 42 }
];
splat(get('oranges'))(inventories)
//=> [ 144, 54, 12 ]
That’s nicer than writing things out “longhand:”
splat(function (inventory) { return inventory.oranges })(inventories)
//=> [ 144, 54, 12 ]
Ruby users recognize get
, it’s equivalent to Symbol#to_proc
, the method that allows them to write inventory.map &:oranges
instead of using a block.
Pluck
This pattern of combining splat
and get
is very frequent in JavaScript code. So much so, that we can take it up another level:
function pluck (attr) {
return splat(get(attr))
}
pluck('eggs')(inventories)
//=> [ 36, 12, 42 ]
Libraries like Underscore provide pluck
in a different form:
_.pluck(inventories, 'eggs')
//=> [ 36, 12, 42 ]
Our recipe is terser when you want to name a function:
var eggsByStore = pluck('eggs');
vs.
function eggsByStore (inventories) {
return _.pluck(inventories, 'eggs')
}
Maybe
A common problem in programming is checking for null
or undefined
(hereafter called “nothing,” while all other values including 0
, []
and false
will be called “something”). Languages like JavaScript do not strongly enforce the notion that a particular variable or particular property be something, so programs are often written to account for values that may be nothing.
This recipe concerns pattern that is very common: A function fn
takes a value as a parameter, and its behaviour by design is to do nothing if the parameter is nothing:
function isSomething (value) {
return value != null
}
function checksForSomething (value) {
if (isSomething(value)) {
// function's true logic
}
}
Alternately, the function may be intended to work with any value, but the code calling the function wishes to emulate the behaviour of doing nothing by design when given nothing:
var something = isSomething(value) ?
doesntCheckForSomething(value) : value;
Naturally, there’s a recipe for that, borrowed from Haskell’s maybe monad, Ruby’s andand, and CoffeeScript’s existential method invocation:
function maybe (fn) {
return function () {
var i;
if (arguments.length === 0) {
return
}
else {
for (i = 0; i < arguments.length; ++i) {
if (arguments[i] == null) return arguments[i]
}
return fn.apply(this, arguments)
}
}
}
maybe
reduces the logic of checking for nothing to a function call, either:
function checksForSomething = maybe(function (value) {
// function's true logic
});
Or:
var something = maybe(doesntCheckForSomething)(value);
Now let’s look an an elegant use for maybe
. You recall get
from above? get('name')
acts like function (obj) { return obj.name }
You can use get
with .map
: arrayOfObjects.map(get('name'))
or with splat
: splat(get('name'))(arrayOfObjects)
. Now consider: What if arrayOfObjects
is a sparse array? If some of its entries are null
?
maybe
to the rescue:
arrayOfObjects.map(maybe(get('name')))
This maps the array, getting the name if there is a value.
Summary of Part I
- We’ve seen four handy combinators: “get,” “splat,” “pluck,” and “maybe.”
- “Get” and “maybe” play well together; “splat” and “pluck” are conveniences that help program in a functional rather than OO style.
In Part II, we’ll look at a more complex recipe, “partial,” and some combinators that are specifically tuned for working with instance methods: “bound,” “send,” and “fluent.” (cough). The recipes in this post are from the book JavaScript Allongé, a book focused on working with functions in JavaScript, including combinators, constructors, methods, and decorators. You can download a free sample PDF.
Feedback welcome, or discuss these ideas on hacker news.