Functional programming in JavaScript for people who think functional programming is weird and hard
Table of contents
(Hat tip to Chris Coyier for inspiring the title.)
What if I told you that you never had to write a for
loop ever again? And in the process, your code would be clearer, shorter, and have fewer bugs?
Sound good? Functional programming might be for you.
Quick aside
People who are already down with functional programming probably won’t find a lot of interest here. Also, Functional Programming™ in the eye-crossing Wikipedia-approved definition sense is a huge, complicated topic. So when I’m saying “functional programming”, I mostly mean “using functions to do stuff in a clever way”. I am aware that this is just scratching the surface.
Here we go
Here’s a situation from the the other day at work.
We were hoping to show a chart with memory usage over time, like this:
To do so, we needed data. The data came to us as some JSON that looked like this:
[
[242, 1428771827000],
[256, 1428771828000],
[128, 1428771829000],
[null, 1428771830000],
[128, 1428771831000]
// and so on
]
Well, that’s totally unclear!
[/has a chat with the developer who wrote the undocumented API…]
OK! So we get an array of two-item arrays. The first entry in each array is the memory usage in megabytes; the second entry is the timestamp. At some points, the service would be busted and not using memory at all, so those are the null
values.
To use that data in our chart library, we needed to (1) throw out any of the null
values because who cares and (2) convert each array to an object, like this:
[
{ "value": 242, "timestamp": 1428771827000 },
{ "value": 256, "timestamp": 1428771828000 },
{ "value": 128, "timestamp": 1428771829000 },
// throw out this pair that had the "null" value
{ "value": 128, "timestamp": 1428771831000 }
]
To do just that, I found this function in our codebase:
function _modifyDataForChart(inputArray) {
var dataArray = []
for (var i = 0; i < inputArray.length; i++) {
var cur = inputArray[i]
if (cur[0] != null) {
dataArray.push({
timestamp: cur[1],
value: cur[0],
})
}
}
return dataArray
}
We create a new variable dataArray
as an empty array. Then you do a for
loop through every entry of the given inputArray
, and do something with each item. Pretty standard stuff.
However, the for
loop alone doesn’t tell us exactly what we’re doing to the array. Or, really, if we’re doing anything at all. It’s just a loop. Let’s a take a look at just that for
block:
for (var i = 0; i < inputArray.length; i++) {
var cur = inputArray[i]
if (cur[0] != null) {
dataArray.push({
timestamp: cur[1],
value: cur[0],
})
}
}
So we make a new cur
variable for the current array item. Then we check that the first value in that array is null
. If it isn’t, we add a new object onto our final array using dataArray.push()
.
Again, pretty standard stuff. But I had to examine every bit of that for
loop to tell you that. The for
loop, on its own, doesn’t announce that you intend to throw out some items and transform the rest. Plus now you have to deal with x
and cur
and keep those straight.
You’re bored now. I’m boring you. Let’s make this interesting.
What are we actually doing with for (var i = 0; i < inputArray.length; i++) { /* do something */ }
?
You would probably say, “I can’t be certain, but we’re likely going to do something with every item in inputArray
”. Yup!
Put that another way: “given an array of items, do something with every item”. You do that a lot, right? And when you do something a lot, it’s a good idea to write a function for it, right? How about this:
function forEach(array, doSomething) {
for (var i = 0; i < array.length; i++) {
doSomething(array[i])
}
}
That’s about all you need: pass in an array and a function that says what to do for every item in the arary. Boom: with this one function you don’t need to write for
loops almost ever again.
With forEach(inputArray, doSomething)
, we are saying more clearly: “do something for every item in inputArray
”. Compare that with for (var i = 0; i < inputArray.length; i++)
where we have to spell out “yes, of course, start at 0
like we almost always do, go the whole length of the array like always, add 1 each time like almost always.
This, my friends, is what we’re going for with functional programming: using some well-considered functions to describe what you’re doing, instead of writing tons of variables and loops to tell the stupid computer how to do it over and over again.
There’s two tricks here: the first is even thinking to create a function that does those messy for
loops for you. You can totally do that! Make the machines do the work for you!
The second trick is thinking about “do something with every item” as another function: doSomething(item)
. Because JavaScript lets you pass around functions like you would pass around anything else, this sort of abstraction is possible. doSomething
could be anything!
Use standard functions! Like write every item to the console:
forEach(array, console.log.bind(console))
Name your own functions and use those!
function doSomeStuff(item) {
/* do some stuff with item */
}
forEach(array, doSomeStuff)
Or write a new anonymous function on the spot, cowboy:
forEach(array, function (item) {
/* do some stuff with item */
})
So how does my little chart data function look if we have forEach
in our toolbox?
function _modifyDataForChart(inputArray) {
var dataArray = []
forEach(inputArray, function (item) {
if (item[0] != null) {
dataArray.push({
timestamp: item[1],
value: item[0],
})
}
})
return dataArray
}
A little cleaner. No for
loop, at least! Let’s do better.
Read the dang manual and you’ll see that we are not totally original thinkers here: arrays have their own method to do pretty much exactly the same thing: Array.forEach
.
function _modifyDataForChart(inputArray) {
var dataArray = []
inputArray.forEach(function (item) {
if (item[0] != null) {
dataArray.push({
timestamp: item[1],
value: item[0],
})
}
})
return dataArray
}
Now you don’t need your own forEach
function somewhere else.
But we can do so much better.
Let’s back up a bit: I mentioned above that our goal with the _modifyDataForChart
function was to do two things:
- remove any entry where the value part of the array is
null
- convert each entry from an array to an object that our chart library can use
Those are my particular needs, but let’s think of those as very generic goals:
- remove some entries from an array
- change each entry in an array
Those are such common goals that arrays have their own functions to do exactly that: Array.filter()
and Array.map()
.
Let’s start with Array.filter
. Like Array.forEach
, you pass in a function that then gets called for each item in the array.
But now there’s a twist: our function shouldn’t do something with an item; it must test something about an item and return true
or false
. If the function returns true
, the element stays in the array. If it returns false
-like, we leave it out.
If you were writing your own filtering function, it might look this:
function filter(array, testSomething) {
var filteredArray = []
for (var i = 0; i < array.length; i++) {
if (testSomething(array[i])) {
filteredArray.push(array[i])
}
}
return filteredArray
}
That’s just to illustrate what’s going on. With Array.filter
, .filter
is a method on an existing array, so you only need to pass in the function that will test every item in the array.
Here’s our sample rewritten to use Array.filter
(we’ll get to transforming in a moment):
function _modifyDataForChart(inputArray) {
var dataArray = inputArray.filter(function (item) {
return item[0] != null
})
/*
still need to transform the items into objects!
*/
return dataArray
}
Because Array.filter
works the same if you return truthy or false-y values, we can get even terser:
function _modifyDataForChart(inputArray) {
var dataArray = inputArray.filter(function (item) {
return item[0] // kick it out if null, '', 0, false, etc
})
/*
still need to transform the items into objects!
*/
return dataArray
}
(This is assuming that our data won’t ever be something valid but false-y like 0
.)
That’s Array.filter
. Now you can stop making new arrays, looping through an existing array, adding items to your new array if they pass some test, and then going forward with the new array.
Man, I feel better just imagining all that code I’m not writing.
Now let’s look at transforming all the remaining items with Array.map
.
Think of it like Array.map( transformTheItem )
. Like Array.forEach
and Array.filter
, we supply a function to run with every item in an array. This time, the function must return a new value, and in the end we get an array of those new values.
If you were writing your own raw function to transform every item in an array, it might look like this:
function map(array, transform) {
var transformedArray = []
for (var i = 0; i < array.length; i++) {
var newItem = transform(array[i])
transformedArray.push(newItem)
}
return transformedArray
}
Again, that’s just to illustrate what’s roughly going on behind the scenes. Just like Array.filter
, Array.map
is a method off an existing array, so you only need to pass in the function that will transform every item in the array. Here’s my chart data function rewritten to use Array.map
:
function _modifyDataForChart(inputArray) {
var dataArray = inputArray.filter(function (item) {
return item[0] // kick it out if null, '', 0, false, etc
})
dataArray = dataArray.map(function (item) {
return {
timestamp: item[1],
value: item[0],
}
})
return dataArray
}
Oh! But both Array.filter
and Array.map
return the new arrays, so we can chain .map
right on to .filter
:
function _modifyDataForChart(inputArray) {
var dataArray = inputArray
.filter(function (item) {
return item[0]
})
.map(function (item) {
return {
timestamp: item[1],
value: item[0],
}
})
return dataArray
}
Oh! But here we’re just immediately returning that new dataArray
, so we can just return the whole expression and skip the extra variable:
function _modifyDataForChart(inputArray) {
return inputArray
.filter(function (item) {
return item[0]
})
.map(function (item) {
return {
timestamp: item[1],
value: item[0],
}
})
}
To save you some scrolling, here’s what we were originally working with:
function _modifyDataForChart(inputArray) {
var dataArray = []
for (var i = 0; i < inputArray.length; i++) {
var cur = inputArray[i]
if (cur[0] != null) {
dataArray.push({
timestamp: cur[1],
value: cur[0],
})
}
}
return dataArray
}
Isn’t our new filter/mappy version prettier? And it says exactly what we’re doing: first we’re filtering out some items, then we’re transforming each item to objects. A few less lines of code than we had before, too. Win win win.
Bonus round #1: lodash
The above is all using built-in Array methods. You can start having even more fun if you look into libraries for common functional programming tools. Scan the docs for, say, lodash and you’ll probably recognize an astonishing number of common patterns.
For example, if you think about what we were doing with our map
function, all we were doing is taking values in a array and labeling them to make a new object. This is such a common pattern that the lodash method _.zipObject
does exactly that, pairing up an array of keys with an array of values to build an object:
_.zipObject(['value', 'timestamp'], [256, 1428771828000])
// -> { value: 256, timestamp: 1428771828000 }
I like that you just list the keys in order to match with the array of values. Seems very clear to me.
If we include the lodash library, let’s use .zipObject
in our .map()
function:
function _modifyDataForChart(inputArray) {
return inputArray
.filter(function (item) {
return item[0]
})
.map(function (item) {
return _.zipObject(['value', 'timestamp'], item)
// -> basically {value: item[0], timestamp: item[1]}
})
}
That’s probably about as far as I would go here, but let’s explore some more.
You might think that it’d be nice to not write function(item) {}
all the dang time. Like all we’re really doing with…
.map(function (item) {
return _.zipObject(['value', 'timestamp'], item)
})
… is passing each item
into the same _.zipObject
function each time. Wouldn’t it be nice if you could just do this?
// this will not work!!
.map(_.zipObject(['value', 'timestamp']))
What you want there is called a partially applied function. The idea is you take a function that requires many arguments, pre-apply some, but not all, of those arguments, and create a new function that is just waiting for the remaining arguments.
Like this new function:
function zipChartData(values) {
return _.zipObject(['value', 'timestamp'], values)
}
Now we have zipChartData
, a new function that calls _.zipObject
with your pre-defined keys. Now you only need to provide the array of values.
But, again, we’re not the first people to think of this! lodash has a method for exactly this called _.partial()
:
// roughly equivalent
function oldSchoolZipChartData(values) {
return _.zipObject(['value', 'timestamp'], values)
}
var newSchoolZipChartData = _.partial(_.zipObject, ['value', 'timestamp'])
Now you can pass it right into .map()
without creating an anonymous function:
var zipChartData = _.partial(_.zipObject, ['value', 'timestamp'])
function _modifyDataForChart(inputArray) {
return inputArray
.filter(function (item) {
return item[0]
})
.map(zipChartData)
}
Or skip the new variable again, but I think that’s getting harder to read:
function _modifyDataForChart(inputArray) {
return inputArray
.filter(function (item) {
return item[0]
})
.map(_.partial(_.zipObject, ['value', 'timestamp']))
}
If you’re big into jQuery, you might like the chain capability of lodash:
function _modifyDataForChart(inputArray) {
return _(inputArray)
.filter(function (item) {
return item[0]
})
.map(_.partial(_.zipToObj, ['value', 'timestamp']))
.value() // closes out the chain and returns the final array
}
Now we’re using lodash’s version of filter
and map
instead of the built-in Array methods.
lodash’s _.filter()
has a shortcut where instead of the testing function you can just provide an object key to look up and test for truthiness:
// roughly equivalent
anArray.filter(function (item) {
return item['thing']
})
_.filter(anArray, 'thing')
Both of those are saying: only give me the items in this array where item.thing
is truthy.
That’s exactly what we’re doing with our filtering function, so we could switch the order—turn the arrays into objects first— in order to use lodash’s shortcut format, like this:
function _modifyDataForChart(inputArray) {
return _(inputArray)
.map(_.partial(_.zipToObj, ['value', 'timestamp']))
.filter('value')
.value()
}
First, transform every array into an object, then filter out some of them. Lovely.
(Note how in the lodash chain syntax, you don’t pass the initial array into the functions; they’re assumed from the previous function in the chain.)
I don’t love that you have to call .value()
to end the chain and return the final array, but otherwise that code seems pretty easy to follow.
Bonus round #2: Ramda
Since we’re exploring partially applying functions, the Ramda library is similar to lodash, but with an important twist:
Ramda functions are automatically curried. This allows you to easily build up new functions from old ones simply by not supplying the final parameters.
“Curried” is a loaded term, but for our purposes, think of it as basically identical to our use of lodash’s _.partial()
method above, where we provide some arguments ahead of time, and the function will then wait for the remaining arguments. But Ramda does it automatically for every function.
var myTest = function (item) {
/* test something, return true or false */
}
// ----- Returning filtered lists immediately
// Ramda is "R". Note the argument order.
R.filter(myTest, myArray)
// equivalent to:
_.filter(myArray, myTest)
// ----- Partial application:
var myRamdaFilter = R.filter(myTest)
// roughly equivalent to lodash code:
var myLodashFilter = _.partialRight(_.filter, myTest)
// now you can pass an array to myFilter() later
myRamdaFilter(myArray)
In Ramda, every function is A-OK with you not providing all the arguments, and the order of arguments is reversed from lodash: your data comes last instead of first.
The swapped order or arguments and automatic partial application makes it easy to compose new functions out of Ramda’s myriad other functions. As an example, here’s my data transformation and filtering function using R.compose() and other Ramda methods:
var _modifyDataForChart = R.compose(
R.filter(R.prop('value')),
R.map(R.zipObj(['value', 'timestamp']))
)
It takes a little backwards thinking, but this is the least code yet! Now we’re totally out out of the business of writing function
or return
or, really, much of anything besides Ramda
functions.
Bonus round #3: Future javascript
So, a little off-topic, but the next version of JavaScript introduces several additions that make much of this even more fun. I won’t dive into the details here, but with arrow functions, destructuring, and object literal shortcuts, you can do this:
let _modifyDataForChart = (inputArray) => {
return inputArray.filter(([value]) => value).map(([value, timestamp]) => ({ value, timestamp }))
}
No library required!
And with the next next version of JavaScript, you can use array comprehensions to run map and filter operations like this:
let _modifyDataForChart = (inputArray) => {
return [
{value, timestamp}
for ([value, timestamp] of inputArray)
if (value)
]
}
Ha, we’re back to using for
, but doesn’t that look kind of awesome?
Further reading
- Joel Spolsky has an article called “Can Your Programming Language Do This?” that covers similar ground.
- Chapter 6 of the 1st edition of Eloquent JavaScript by Marijn Haverbeke or Chapter 5 of the 2nd edition do a splendid job of exploring functional programming.
- OK, sure, you’re probably not going to read documentation for fun but the docs for popular functional programming libraries like Underscore, lodash, or Ramda are worth a scan just to see how folks are tackling different abstractions of common programming tasks.
- Work your way through “Professor Frisby’s Mostly Adequate Guide to Functional Programming”. I just finished the chapters on currying and function composition and my brain is a little puddle.
- “You Might Not Need Underscore” looks in greater detail at how the next version of JavaScript can save you a library function here and there.
- “You might not need a loop” by Ire Aderinokun talks more about when or when not to use the array methods.