The Best Way To Compute The Average Age Of Cat Owners In Functional Programming
In JavaScript, there is an ever-increasing adoption of functional style programming in our modern web applications. And, even in code that isn't purely functional, developers are often using functional aspects to map, reduce, filter, and sort values. It's really cool (and really fun) stuff; and with excellent support for ES5 in modern browsers, functions like .map(), .filter(), .reduce(), and Object.keys() can even be used without the help of libraries like Lodash.
With so much functionality at our fingertips, it can be hard to know the best way to do something. Take, for example, trying to find the average age of cat owners in a given collection. With functional programming, we have a lot of ways to get to the same answer. Here are just a few that I could come up with:
var _ = require( "lodash" );
// Assume a collection that has:
// - age: Number
// - hasCat: Boolean
var people = require( "./people.json" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var reduction = _.reduce(
people,
function operator( accumulator, person ) {
if ( person.hasCat ) {
accumulator.sum += person.age;
accumulator.count++;
accumulator.average = ( accumulator.sum / accumulator.count );
}
return( accumulator );
},
{
sum: 0,
count: 0,
average: 0
}
);
console.log( "Average age of cat owner: %s", reduction.average );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var averageAge = _.reduce(
_.pluck(
_.where(
people,
{
hasCat: true
}
),
"age"
),
function operator( average, age, index, collection ) {
return( average + ( age / collection.length ) );
},
0
);
console.log( "Average age of cat owner: %s", averageAge );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var catOwners = _.where( people, { hasCat: true } );
var averageAge = ( _.sum( _.pluck( catOwners, "age" ) ) / catOwners.length );
console.log( "Average age of cat owner: %s", averageAge );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
var averageAge = _.chain( people )
.where({ hasCat: true })
.pluck( "age" )
.reduce(
function operator( sum, age, index, collection ) {
return ( index === ( collection.length - 1 ) )
? ( ( sum + age ) / collection.length )
: ( sum + age )
;
},
0
)
.value()
;
console.log( "Average age of cat owner: %s", averageAge );
These are all somewhat similar and somewhat different. And, they all do the same thing - compute the average age of cat owners. But, which way is the best?
None of them. They're all bad.
The best way to compute the average age of cat owners would be to write:
console.log( "Average age of cat owner: %s", getAverageAgeOfCatOwners( people ) );
So, clearly this is a little tongue-in-cheek. But, I just wanted to throw this out there as a gentle reminder that the readability of code is super important. Just because something can be computed with a complex combination and application of functions, it doesn't mean that it's easy to read or to understand. In functional programming, you can still hide implementation details. You can still create code that is obivous in its intent. And, more importantly, obvious to the next developer.
Want to use code from this post? Check out the license.
Reader Comments
Do you also advocate the usage of parseInt() for converting a string to an integer, rather than the unary + operator, or multiplying by 1, since parseInt() more clearly expresses intent? I myself do.
@George,
Honestly, I go back and forth on what I feel comfortable with. I use all of them a bit, in different situations. The problem with parseInt() is that most people forget to pass-in the base-10 radix as the second, optional parameter. The browsers may default to base-10 now-a-days; but, I'm pretty sure this used to cause some weird "octal" edge-case bugs.
If I can encapsulate the access to the value, such as something like:
function getUserID() {
. . . return( + $routeParams.userID || 0 );
}
... then I'll usually use the "+" operator (or at least what I've been doing lately). But, I am fine with that because the parent function is what has to be most understandable. Then, the parsing just becomes an implementation detail that can be swapped out.
So, long story short, I have used all three approaches. And in each situations, I probably [hopefully] try to choose the one that seems like it has the least amount of "magic".
Yeah the optional second argument is something to consider with parseInt() and I indeed ran into one of those octal issues back in 2001 IIRC. Speaking of magic though, I rely on some of parseInt's magic, such as ignoring any trailing non-digit input -- it's really great for instance when your input is something such as '20px' but you just want 20 as an integer from it.