Chapter 4: Currying
Can't live if livin' is without you
My Dad once explained how there are certain things one can live without until one acquires them. A microwave is one such thing. Smart phones, another. The older folks among us will remember a fulfilling life sans internet. For me, currying is on this list.
The concept is simple: You can call a function with fewer arguments than it expects. It returns a function that takes the remaining arguments.
You can choose to call it all at once or simply feed in each argument piecemeal.
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
Here we've made a function add
that takes one argument and returns a function. By calling it, the returned function remembers the first argument from then on via the closure. Calling it with both arguments all at once is a bit of a pain, however, so we can use a special helper function called curry
to make defining and calling functions like this easier.
Let's set up a few curried functions for our enjoyment.
var curry = require('lodash/curry');
var match = curry(function(what, str) {
return str.match(what);
});
var replace = curry(function(what, replacement, str) {
return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
return ary.filter(f);
});
var map = curry(function(f, ary) {
return ary.map(f);
});
The pattern I've followed is a simple, but important one. I've strategically positioned the data we're operating on (String, Array) as the last argument. It will become clear as to why upon use.
match(/\s+/g, 'hello world');
// [ ' ' ]
match(/\s+/g)('hello world');
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces('hello world');
// [ ' ' ]
hasSpaces('spaceless');
// null
filter(hasSpaces, ['tori_spelling', 'tori amos']);
// ['tori amos']
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(/\s+/g) }) }
findSpaces(['tori_spelling', 'tori amos']);
// ['tori amos']
var noVowels = replace(/[aeiouy]/ig);
// function(replacement, x) { return x.replace(/[aeiouy]/ig, replacement) }
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiouy]/ig, '*') }
censored('Chocolate Rain');
// 'Ch*c*l*t* R**n'
What's demonstrated here is the ability to "pre-load" a function with an argument or two in order to receive a new function that remembers those arguments.
I encourage you to npm install lodash
, copy the code above and have a go at it in the REPL. You can also do this in a browser where lodash or ramda is available.
More than a pun / special sauce
Currying is useful for many things. We can make new functions just by giving our base functions some arguments as seen in hasSpaces
, findSpaces
, and censored
.
We also have the ability to transform any function that works on single elements into a function that works on arrays simply by wrapping it with map
:
var getChildren = function(x) {
return x.childNodes;
};
var allTheChildren = map(getChildren);
Giving a function fewer arguments than it expects is typically called partial application. Partially applying a function can remove a lot of boiler plate code. Consider what the above allTheChildren
function would be with the uncurried map
from lodash (note the arguments are in a different order):
var allTheChildren = function(elements) {
return _.map(elements, getChildren);
};
We typically don't define functions that work on arrays, because we can just call map(getChildren)
inline. Same with sort
, filter
, and other higher order functions(Higher order function: A function that takes or returns a function).
When we spoke about pure functions, we said they take 1 input to 1 output. Currying does exactly this: each single argument returns a new function expecting the remaining arguments. That, old sport, is 1 input to 1 output.
No matter if the output is another function - it qualifies as pure. We do allow more than one argument at a time, but this is seen as merely removing the extra ()
's for convenience.
In summary
Currying is handy and I very much enjoy working with curried functions on a daily basis. It is a tool for the belt that makes functional programming less verbose and tedious.
We can make new, useful functions on the fly simply by passing in a few arguments and as a bonus, we've retained the mathematical function definition despite multiple arguments.
Let's acquire another essential tool called compose
.
Chapter 5: Coding by Composing
Exercises
A quick word before we start. We'll use a library called Ramda which curries every function by default. Alternatively you may choose to use lodash/fp which does the same and is written/maintained by the creator of lodash. Both will work just fine and it is a matter of preference.
There are unit tests to run against your exercises as you code them, or you can just copy-paste into a JavaScript REPL for the early exercises if you wish.
Answers are provided with the code in the repository for this book. Best way to do the exercises is with an immediate feedback loop.
var _ = require('ramda');
// Exercise 1
//==============
// Refactor to remove all arguments by partially applying the function.
var words = function(str) {
return _.split(' ', str);
};
// Exercise 1a
//==============
// Use map to make a new words fn that works on an array of strings.
var sentences = undefined;
// Exercise 2
//==============
// Refactor to remove all arguments by partially applying the functions.
var filterQs = function(xs) {
return _.filter(function(x) {
return match(/q/i, x);
}, xs);
};
// Exercise 3
//==============
// Use the helper function _keepHighest to refactor max to not reference any
// arguments.
// LEAVE BE:
var _keepHighest = function(x, y) {
return x >= y ? x : y;
};
// REFACTOR THIS ONE:
var max = function(xs) {
return _.reduce(function(acc, x) {
return _keepHighest(acc, x);
}, -Infinity, xs);
};
// Bonus 1:
// ============
// Wrap array's slice to be functional and curried.
// //[1, 2, 3].slice(0, 2)
var slice = undefined;
// Bonus 2:
// ============
// Use slice to define a function "take" that returns n elements from the beginning of an array. Make it curried.
// For ['a', 'b', 'c'] with n=2 it should return ['a', 'b'].
var take = undefined;