The Hidden Benefit of Writing Smaller Functions

Dakota Lee Martinez
10 min readMar 15, 2017

--

I remember reading a passage on Quora a few years ago that really stuck with me. I’ve lost touch with the exact source, but from what I remember, the author was in the role of interviewing/hiring developers. The author’s basic idea was that experienced developer’s wrote smaller functions. Of course, it wasn’t until much later that I discovered the hidden benefit of writing smaller functions.

Optimization vs Doing Less

I think when I initially encountered this idea, I thought that it meant that experienced developers knew clever tricks for accomplishing tasks in fewer lines of code. While that is undoubtedly the case, there was something I was missing. I thought that being clever just to bum a few lines from a function definition might make the code more difficult to follow. That didn’t seem like a desirable outcome to me! (I’ve revisited code I wrote a couple of years ago and been shocked by how difficult it is to follow.)

There was something I still didn’t see. Actually, writing smaller functions wasn’t just about optimizing or rewriting the code to occupy fewer lines. The beauty of writing smaller functions is that you’re writing functions that do less. The missing piece for me was that writing smaller functions is mostly a practice in breaking larger functions into pieces.

Instead of one long function that contains all the logic for completing a complicated task with multiple steps and sub-tasks, we have a bunch of functions that break the problem into small pieces. Of course, one function will inevitably be used to put all of the pieces together, but that should be its one job. Ultimately, writing smaller functions makes it much easier to follow the single responsibility principle.

Naming the Hidden Benefit

When we start writing smaller functions, we discover the hidden benefit of more names. Since we’re writing more functions, we have to create more names. Because each function must have a name describing what it does, our code has more names in it. If we name our functions carefully, we now have a roadmap describing the path through our code. This allows us to reason much more clearly about what our code is doing and how it’s doing it. Because, adding more names forces us to be separate the what from the how.

To demonstrate the difference, I’ve started an algorithm challenge that uses the one big long function approach. First, let me introduce the problem. We’re given two arrays as inputs. One is an array of integers representing coins of a certain value. The other is an array of equal length representing the quantity of each value coin we have. Our task is to return the number of possible sums we can make given the information in both arrays.

Here are the constraints for our inputs:

1 ≤ coins.length ≤ 20,
1 ≤ coins[i] ≤ 10000
quantity.length = coins.length,
1 ≤ quantity[i] ≤ 100000
We are also guaranteed that: (quantity[0] + 1) * (quantity[1] + 1) * … * (quantity[quantity.length — 1] + 1) <= 1000000

First, I wanted to get a sense of how my algorithm would work on one of the smaller test cases. I was originally trying to avoid having 3 nested loops, so I started with this:

function possibleSums(coins, quantity) {
var coinValues = quantity.reduce(function(values, q, index){
if(q > 1) {
var newArr = new Array(q-1).fill(coins[index]);
values = values.slice(0, index).concat(newArr).concat(values.slice(index));
}
return values;
},coins);
console.log('coinValues:', coinValues);
var sums = coinValues.reduce(function(obj, coin){
if (!obj.hasOwnProperty(coin)){
obj[coin] = true;
}
return obj;
},{});
console.log('sums:', sums);
for (var i = 0 ; i < coinValues.length ; i++) {
for (var j = i + 1 ; j < coinValues.length; j++ ) {
var sum = coinValues[i] + coinValues[j];
if(!sums.hasOwnProperty(sum)) {
sums[sum] = true;
}
}
}
for (var i = 0 ; i < coinValues.length ; i++) {
for (var j = i + 2 ; j < coinValues.length; j++) {
var sum = coinValues[i] + coinValues[i+1] + coinValues[j];
if(!sums.hasOwnProperty(sum)) {
sums[sum] = true;
}
}
}
for (var i = 0 ; i < coinValues.length ; i++) {
for (var j = i + 3 ; j < coinValues.length; j++) {
var sum = coinValues.slice(i,j+1).reduce(function(a,b){return a+b});
if(!sums.hasOwnProperty(sum)) {
sums[sum] = true;
}
}
}
return Object.keys(sums).length;
}

The basic goal is to store each unique sum as a key on the sums object. This is basically a hard coded solution to one of the shorter test cases. It will work if the coins and quantity arrays are both only 3 items long. After that, it becomes clear that we actually need another loop.

Beyond being quite inefficient, this first stab at a solution is also difficult to understand, extend and reason about. It was hard for me to write the code this way, as I had to keep fighting the urge to pull out functions. Most notably, I wanted to extract functions to persist data on the possible sums. I also wanted a function to collect all of the possible sums that can be created by a single coin (in different quantities).

When the logic grows in length, it requires an increasing amount of mental overhead to keep focused on the problem. It was only after I split off a couple of functions that I came to a solution. I’ve rewritten that solution as one long function below.

function possibleSums(coins, quantity) {
var hasSum = {};
coins.reduce(function(possibilities, coin, index){
var newSums = [];
var coinCombos = [];
for (var i = 0 ; i <= quantity[index] ; i++) {
coinCombos.push(coin * i);
}
possibilities.forEach(function(previousSum, index){
for(var i = 0 ; i < coinCombos.length; i++) {
var thisSum = coinCombos[i] + previousSum;
if(!hasSum.hasOwnProperty(thisSum)){
hasSum[thisSum] = true;
newSums.push(thisSum);
}
}
});
possibilities = possibilities.concat(newSums);
return possibilities;
},[0]);

return Object.keys(hasSum).length - 1;
}

This is an improvement, to be sure. I’ve spent more time thinking about the variable names and I’ve used them to help clarify my approach. Still, this function is long and difficult to follow. There is also some repetition that can be avoided. The main thing to note at the moment is how much the code has improved given the extra time and attention paid to the naming of variables within the function.

Extracting Smaller Functions

First, let’s take out a function that creates an array of possible sums we can make by choosing a different quantity for a single coin. This array was built within our coins.reduce callback (lines 5-8 above) and used inside the nested possibilities.forEach callback (lines 10-11 above). Also, it's a simple pattern using a for loop to add to an array that's taking up extra space in our function. Pulling it out into a smaller function will bring the focus to a higher level where we can see how the pieces are working together.

function possibleSums(coins, quantity) {
var hasSum = {};
coins.reduce(function(possibilities, coin, index){
var newSums = [];
var coinCombos = getPossibleSumsForCoin(coin, quantity[index]);
possibilities.forEach(function(previousSum, index){
for(var i = 0 ; i < coinCombos.length; i++) {
var thisSum = coinCombos[i] + previousSum;
if(!hasSum.hasOwnProperty(thisSum)){
hasSum[thisSum] = true;
newSums.push(thisSum);
}
}
});
possibilities = possibilities.concat(newSums);
return possibilities;
},[0]);

return Object.keys(hasSum).length - 1;
}
function getPossibleSumsForCoin(coin, quantity){
var coinCombos = [];
for (var i = 0 ; i <= quantity; i++) {
coinCombos.push(coin * i);
}
return coinCombos;
}

Notice that the readability of the algorithm has improved quite a bit with the replacement of the for loop with the function getPossibleSumsForCoin. It is much clearer what the for loop was doing now that it's replaced by a reference to a descriptively named function. We've separated the what from the how.

Using Closure to Count Possible Sums

Next, I was itching to separate the persistence of possible sums. To do this, I created a UniqueSumCounter function that creates a closure over a sums object that contains all of the possible sums encountered thus far. The function returns an object that responds to two functions, add(sum) and total(). This allows a neat encapsulation of adding and retrieving new possible sums. In order to be of extra use in our control flow, add(sum) will return false if the sum was already recorded.

function possibleSums(coins, quantity) {
var sums = UniqueSumCounter();
coins.reduce(function(possibilities, coin, index){
var newSums = [];
var coinCombos = getPossibleSumsForCoin(coin, quantity[index]);
possibilities.forEach(function(previousSum, index){
for(var i = 0 ; i < coinCombos.length; i++) {
var thisSum = coinCombos[i] + previousSum;
if(sums.add(thisSum)){
newSums.push(thisSum);
}
}
});
possibilities = possibilities.concat(newSums);
return possibilities;
},[0]);

return sums.total();
}
function getPossibleSumsForCoin(coin, quantity){
var coinCombos = [];
for (var i = 0 ; i <= quantity; i++) {
coinCombos.push(coin * i);
}
return coinCombos;
}
function UniqueSumCounter() {
var sums = {};

return {
add: add,
total: total
};

function add(sum) {
if (!sums.hasOwnProperty(sum)) {
sums[sum] = true;
return true;
}
return false;
}

function total() {
return Object.keys(sums).length - 1;
}
}
console.log(possibleSums([10,50,100,500],[5,3,2,2])); // => 122
console.log(possibleSums([10,50,100],[1,2,1])) // => 9
console.log(possibleSums([1],[5])) // => 5

Notice how our possibleSums function has become slightly more readable. Now, we can take a look through it and get a better sense of what it's doing before we get into how it's doing it. By extracting smaller functions and using more names, we've made our code easier to read, follow and understand. Of course, there's a trade off with this approach, as we now have nearly twice as many lines of code. The difference is that our code is much easier to follow.

Still, there’s a big part of our possibleSums function that's still saying how instead of just what. Inside of the coins.reduce callback, we've got a forEach loop that contains a nested for loop.

This part of the code is responsible for gathering the new possible sums we can create each time we include another one of the coins. It does this by iterating over all of the previous sum possibilities and adding each of the sums we can create with the current coin. If the sum is not already stored in memory as a possible sum, then we add it to our newSums array. Once we're finished going through all of the new possibilities, we concatenate the newSums array onto the array of possible sums (possibilities) and move on to the next coin.

Smaller Functions Help Us to Be Explicit About Intentions

We can dramatically improve the readability of our possibleSums function by extracting another function to handle this task. First, let's take a look at what information that code needs access to:

*possibilities: the array of possible sums created by combinations of previous coins
*currentCoinSums: the array of possible sums created by choosing a different amount of the current coin.
*sums: an instance of the UniqueSumCounter() closure that tracks the possible sums in memory .

Since we need access to sums it makes the most sense to actually add the new function to our UniqueSumCounter() object so we won't have to pass it in as a parameter:

Notice how much more explicit the possibleSums is after extracting this function:

function possibleSums(coins, quantity) {
var sums = UniqueSumCounter();
coins.reduce(function(possibilities, coin, index){
var currentCoinSums = getPossibleSumsForCoin(coin, quantity[index]);
var newSums = sums.combinePossibleSums(possibilities, currentCoinSums);
possibilities = possibilities.concat(newSums);
return possibilities;
},[0]);

return sums.total();
}
function getPossibleSumsForCoin(coin, quantity){
var coinCombos = [];
for (var i = 0 ; i <= quantity; i++) {
coinCombos.push(coin * i);
}
return coinCombos;
}
function UniqueSumCounter() {
var sums = {};

return {
add: add,
total: total,
combinePossibleSums: combinePossibleSums
};

function add(sum) {
if (!sums.hasOwnProperty(sum)) {
sums[sum] = true;
return true;
}
return false;
}

function total() {
return Object.keys(sums).length - 1;
}

function combinePossibleSums(previousSums, currentSums) {
return previousSums.reduce(function(newSums, previousSum){
currentSums.forEach(function(currentSum){
var sum = previousSum + currentSum;
if(add(sum)) {
newSums.push(sum);
}
});
return newSums;
},[]);
}
}
console.log(possibleSums([10,50,100,500],[5,3,2,2])); // => 122
console.log(possibleSums([10,50,100],[1,2,1])) // => 9
console.log(possibleSums([1],[5])) // => 5

One of the obvious downsides here is that the code has actually become more fragmented. In the process of refactoring the solution, I realized that it would actually be easier to encapsulate all of these smaller functions inside of the possibleSums function definition. That way, the sums, coins, and quantity are all available to the child functions. This is my favorite iteration yet, because it is the easiest to follow and requires fewer variable definitions and jumping around with the eyes to follow. Also, it resolves one of the things that was bothering me about the previous solution. Namely, that I had an array of possibilities and also an object storing possibilities. This was both a waste of memory and a potential source of confusion. So, I'm all the happier with the following solution:

function possibleSums(coins,quantity) {
var sums = {'0': true};

return calculate();

function calculate() {
coins.forEach(function(coin,index){
var currentCoinSums = getPossibleSumsForCoin(coin, quantity[index]);
combinePossibleSums(currentCoinSums);
});
return total();
}

function getPossibleSumsForCoin(coin, quantity){
var coinCombos = [];
for (var i = 0 ; i <= quantity; i++) {
coinCombos.push(coin * i);
}
return coinCombos;
}

function combinePossibleSums(currentSums) {
return Object.keys(sums).forEach(function(previousSum){
currentSums.forEach(function(currentSum){
var sum = Number(previousSum) + currentSum;
add(sum);
});
});
}

function add(sum) {
if (!sums.hasOwnProperty(sum)) {
sums[sum] = true;
}
}

function total() {
return Object.keys(sums).length - 1;
}

}

One thing that this last solution illustrates really well is the power of closure to enable object oriented programming in JavaScript. The beauty of object oriented programming in JavaScript is that instead of having to create a class and then creating instances for solutions, we can just call the function to retrieve a solution object. For instance, it would be a trivial change to adjust the return value of the function to be an object instead of a single value. Then, we could also expose the final value of sums, perhaps in array form using Object.keys(sums). That way, we could have solution objects that not only tell us how many possible sums we can make, but also what all the possibilities are!

Final Thoughts

Clearly, there’s a tradeoff to be considered when extracting smaller functions from a long function. We start off with a single function with 21 lines in the block and ended with one parent function with five child functions all fewer than 6 lines in length. Although the whole thing is twice as long, it seems to me to be infinitely easier to follow the logic. Extracting the functions allows a single read through of possibleSums to give a high level understanding of how the solution works. The secret is in that naming those smaller functions allows you to explain what your code is doing as you go!

Originally published at Becoming a Programmer.

--

--