Skip to content

Instantly share code, notes, and snippets.

@remarcmij
Last active March 27, 2018 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save remarcmij/57101c1107f2bd479a162b18a1e7ae01 to your computer and use it in GitHub Desktop.
Save remarcmij/57101c1107f2bd479a162b18a1e7ae01 to your computer and use it in GitHub Desktop.
Functions and callbacks

Example of functions and callbacks

Why use functions?

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:

  1. 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?
  2. 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)

Example

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:

  1. Open each can with a can opener (we have two at our disposal: a manual can opener and an electric one).
  2. Warm up the contents of each can (takes some time).
  3. 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!

The complete code

'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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment