We create and use functions to make our programming life easier, not harder. But in order to use them effectively we must have a sound understanding of how to organize them.
Because our brain limits the complexity that we can manage at any single point in time¹, it is better to carve up a large task into a number of smaller tasks, each doing one thing and doing it well. We can then focus on the small tasks, one at a time: deal with one and move on to the next. This mental strategy of "divide and conquer²" is often referred to as abstraction.
When applying this strategy, we are in the knowledge that each individual task has been well taken care of, and we no longer need to worry about the internal details of each of them. Instead, we can now focus on orchestrating the combined use of the smaller tasks to establish the greater goals of the larger task. If there is a problem somewhere in our code, we can try and find the smaller task that causes the problem, fix it in isolation and not worry about all the other smaller tasks while doing that.
Taking the above into considerations, the most effective JavaScript functions exhibit the following characteristics:
- They are relatively small.
- They do one thing only and do it well.
- They are named to accurately describe what the function does.
- They get all the data they require through arguments, each named to accurately describe what the argument represents. They do not reference global variables.
- If the purpose of the function is to produce a new value from its arguments, that value is returned from the function through a
return
statement. In this case the function should produce no side-effects (see next point). - Functions that do not return a value are said to produce a "side-effect", e.g. writing to the console, generating HTML elements etc.
- If the function produces a value asynchronously, that value must be "returned" by means of a callback or promise.
Notes:
- In psychology this is called "cognitive load": the total amount of mental activity imposed on working memory in any one instant. See for instance: What is cognitive load?
- Divide and conquer is an approach to a problem or task, attempting to achieve an objective by breaking it into smaller parts. Often it is used to separate a force that would be stronger if united, or to cause confusion amongst rival factions. Divide and conquer has applications in many areas, from political science to economic and military strategy. (Source: http://www.axis-and-allies.com/military-tactics-divide-and-conquer.html#w21)
Let's look at a simple example that applies all these principles in practice. The problem at hand is preparing and eating breakfast. The breakfast itself is a bit simplistic: it consist of a can of tomato soup and a can of beans in tomato sauce. The larger task is to:
- Open each can with a can opener (we have two at our disposal: a manual can opener and an electric one).
- Warm up the contents of each can (takes some time).
- Enjoy the warmed up food stuff.
The larger "task" of eating breakfast can thus be broken down into these smaller tasks:
- Opening the first can (we'll use the manual opener: takes time).
- Warming up the first can (takes time).
- Opening the second can (we'll use the electric opener: takes time).
- Warming up the second can (takes time).
- Eat the warmed up contents of the first can.
- Eat the warmed up contents of the second can.
Of course, we could well write this simple example without using any functions or perhaps just one or two but for demonstration purposes, let's use a couple more.
First, we need some way to represent our canned foodstuff. Let's use a JavaScript object for that, using a constructor function.
function FoodCan(contents, weightInGrams) {
this.contents = contents;
this.weightInGrams = weightInGrams;
}
// Examples
const cannedTomatoSoup = new FoodCan('tomato soup', 300);
const cannedBeansInTomatoSauce = new FoodCan('beans', 150);
We need a way to open the cans. As stated, we need to represent two types of can openers. A manual one and an electric one. The manual one takes 4 seconds to open a can. The electric one can do it in a second. When a can is opened, we notify the caller by passing the can's content through the onOpened
callback.
function openManually(foodCan, onOpened) {
setTimeout(() => {
onOpened(foodCan.contents);
}, 4000);
}
function openElectrically(foodCan, onOpened) {
setTimeout(() => {
onOpened(foodCan.contents);
}, 1000);
}
We also need a way to warm up the contents of a can. Let's suppose that different foodstuffs require different warm-up times. Our warmUp()
function takes the content of a can as its foodStuff
argument, a duration
in milliseconds and a callback onReady
to be called when the foodstuff is completely warmed up.
function warmUp(foodStuff, duration, onReady) {
setTimeout(() => {
onReady('hot ' + foodStuff);
}, duration);
}
The process of eating is represented by a function eat()
which just uses console.log
to notify that we are eating and what we are eating.
function eat(foodStuff) {
console.log('Enjoying ' + foodStuff + '.');
}
Notice that so far we haven't talked or thought about how all these small tasks need to be glued together to accomplish the greater goal of the larger task. There was no need to do that up till now. We could just focus on the goal of each small task, one at a time. But now it is time to glue them all together.
Because opening the cans and warming up their content takes time (i.e. is done asynchronously) we need to use callbacks to be notified when these tasks are ready.
Our eatBreakfast()
function takes two cans of foodstuff and an onReady
callback to be called when we have finished eating breakfast.
function eatBreakfast(firstCan, secondCan, onReady) {
openManually(firstCan, contents => {
warmUp(contents, 2000, hotFood => {
const firstHotFood = hotFood;
openElectrically(secondCan, contents => {
warmUp(contents, 2000, hotFood => {
const secondHotFoot = hotFood;
eat(firstHotFood);
eat(secondHotFoot);
onReady();
});
});
});
});
}
Finally, it is time to enjoy our breakfast. Let's make it happen in a function called main()
. This function includes a setInterval
timer that is running while we are waiting for the breakfast to be prepared. It just writes the elapsed seconds to the console. When we have finished our breakfast, the timer is cleared and our program finishes.
function main() {
// The stuff for our breakfast
const cannedTomatoSoup = new FoodCan('tomato soup', 300);
const cannedBeansInTomatoSauce = new FoodCan('beans', 150);
// Show seconds passed
let secondsPassed = 0;
const timerID = setInterval(() => {
secondsPassed += 1;
console.log('seconds passed: ' + secondsPassed);
}, 1000);
// With all this preparation, let's now finally eat our breakfast
eatBreakfast(cannedTomatoSoup, cannedBeansInTomatoSauce, () => {
// stop the interval time
clearInterval(timerID);
});
}
main();
When we run our program you will see the following on the console:
seconds passed: 1
seconds passed: 2
seconds passed: 3
seconds passed: 4
seconds passed: 5
seconds passed: 6
seconds passed: 7
seconds passed: 8
Enjoying hot tomato soup.
Enjoying hot beans.
Note that none of our functions reference global variables. Except for eat()
and main()
, none of the functions produce side-effects. The functions have names that accurately describe what they do. The argument names are an accurate reflection of what their expected value is.
Enjoy your breakfast!
'use strict';
function FoodCan(contents, weightInGrams) {
this.contents = contents;
this.weightInGrams = weightInGrams;
}
function openManually(foodCan, onOpened) {
setTimeout(() => {
onOpened(foodCan.contents);
}, 4000);
}
function openElectrically(foodCan, onOpened) {
setTimeout(() => {
onOpened(foodCan.contents);
}, 1000);
}
function warmUp(foodStuff, duration, onReady) {
setTimeout(() => {
onReady('hot ' + foodStuff);
}, duration);
}
function eat(foodStuff) {
console.log('Enjoying ' + foodStuff + '.');
}
function eatBreakfast(firstCan, secondCan, onReady) {
openManually(firstCan, contents => {
warmUp(contents, 2000, hotFood => {
const firstHotFood = hotFood;
openElectrically(secondCan, contents => {
warmUp(contents, 2000, hotFood => {
const secondHotFoot = hotFood;
eat(firstHotFood);
eat(secondHotFoot);
onReady();
});
});
});
});
}
function main() {
// The stuff for our breakfast
const cannedTomatoSoup = new FoodCan('tomato soup', 300);
const cannedBeansInTomatoSauce = new FoodCan('beans', 150);
// Show seconds passed
let secondsPassed = 0;
const timerID = setInterval(() => {
secondsPassed += 1;
console.log('seconds passed: ' + secondsPassed);
}, 1000);
// With all this preparation, let's now finally eat our breakfast
eatBreakfast(cannedTomatoSoup, cannedBeansInTomatoSauce, () => {
// stop the interval time
clearInterval(timerID);
});
}
main();