Module 7: Further functions

  • how can we use functions to build abstractions?
  • what is pass-by-reference?
  • what is visibility vs scope?

We started out in this course by writing functions with very simple control flow, as in these examples from labs 2 and 3:

Flowchart of the driveForwards() function from lab 2 Flowchart of the turn() function from lab 3

These flowcharts show a very simple control flow: a sequential control flow in which statements are executed, one after the other, until the function is complete. We then added to our function design toolkit by introducing branching or conditional control flow. This used if statements to choose between two alternate possibilities, allowing us to write more interesting functions like onLine() from lab 4:

Flowchart of the onLine() function from lab 4

Then we started designing loops, which also employ conditions but use them to perform actions zero or more times rather than zero times or once. Loops allow us to tackle example problems in places like Project Euler or, in fact, assignment 3! Loops are also going to be a very important part of our remaining labs, and were included in lab 4:

Flowchart of the followLine() function from lab 4

Now we will step back and think a bit more broadly about the functions we’ve been writing. In particular, we will think about how we use functions to build abstractions that help us solve problems.

Call graphs

A graph (in this sense of the word) has nodes and edges. We have already used flowcharts, in which the nodes are process boxes or decision diamonds and the edges represent control flow. Now, however, we can a new kind of graph to our repertoire: the call graph. A call graph shows us how functions call other functions. We can layer this kind of information on top of function flowcharts, as shown here:

Call graph for maze solving

However, at a more abstract level (leaving out the details of what happens inside each function), we can concentrate on the more abstract call graph:

Call graph for maze solving

At each of these function calls, arguments from the function call are passed into parameters. So far in the course, we have seen these arguments be passed by value: the values of the arguments are copied into the parameters so that parameters are independent of the values they were initialized by:

Passing arguments by value

Now, however, we will see a new way of passing arguments: by reference.

Pass-by-reference

Sometimes we don’t want this independence of parameters from the values that are used to initialize them. Instead of passing arguments by value, in which copies get made of all the values, we want to share our variables with a function and give permission for that function to use our variables directly. In C++, this is done using pass-by-reference. In class, we discussed a weather function that would retrieve the current weather from a set of sensors. That function will measure many things and need to “return” many values, but so far we’ve only seen how to return a single value. Using pass-by-reference, however, we can construct the following sort of call:

Passing arguments by reference

In this call, the main function calls weather and gives weather permission to modify some local variables of main. When the weather function takes its measurements, instead of returning them to main, it can write the values directly into those main variables that it was passed references to.

The syntax for C++ pass-by-references requires an additional ampersand (&) between a parameter’s type and name:

/**
 * Report the current weather.
 *
 * @param[out]   temperature    the current temperature [degrees C]
 * @param[out]   humidity       the current humidity [percent]
 *                              @post humidity >= 0 and humidity <= 100
 * @param[out]   windSpeed      the current wind speed [km/h]
 */
void weather(double& temperature, double& humidity, double& windSpeed);

/* ... */

void weather(double& temperature, double& humidity, double& windSpeed)
{
  temperature = /* ... */;
  humidity = /* ... */;
  windSpeed = /* ... */;
}

Apart from this ampersand, the declaration and definition of the weather function look just like something we could’ve written before. What is different now is the meaning (semantics): instead of copying a value into each parameter of weather, we are giving weather access to the local variables of main. An important difference in the call to a pass-by-reference function is that we can only pass variables, not literal values. This is because we are no longer passing values, we’re passing references to variables… so we need variables to have references to!

We can use pass-by-reference to implement lots of functions that would’ve been impossible with the tools we had before, e.g.:

void swap(int& x, int& y)
{
  int temp = x;
  x = y;
  y = temp;
}

Design with functions

Now that we have some notion of how functions work, let’s talk about how to use them in designed solutions to problems. We have seen how functions can help up build up high-level abstractions from lower-level ones, e.g., a function that makes a robot drive forwards will delegate some of its work to lower-level functions that read sensors or turn on the power on motor wheels. This is a bottom-up approach to implementation. Now, however, let’s look at how we can take a top-down approach to problem solving by dividing it into smaller pieces (divide and conquer). This is a common approach in many Engineering disciplines, and it is quite effective in programming.

Procedural decomposition

The top-down approach to procedural decomposition is:

  1. Divide the problem into independent sub-problems that are easier to solve.
  2. Start with a high-level solution to the problem — many steps may be abstract (i.e., some of the implementation details remain unspecified).
  3. Gradually refine solution into an concrete solution (i.e., algorithm) by adding detail.

Example

Problem: write a function to find the total mass of a batch of (identical) washers.

Analysis:

A washer is a doughnut-shaped disc of metal of uniform thickness whose mass is a product of its volume and its density. The mass of a batch would simply be the mass of an individual washer times the number of washers in the batch.

Decomposition:

Find the mass of a batch of washers:

  1. Find the mass, $m$, of an individual washer
  2. Batch mass = $n \times w$

Now we refine the first step:

  1. Find the mass, $m$, of an individual washer
    1. Find the washer’s area, $A$
    2. Find the washer’s volume, $V = A \times d$ (depth)
    3. $m = V \times D$ (density)
  2. Batch mass = $n \times m$

We can also refine the first step of this refined step:

  1. Find the mass, $m$, of an individual washer
    1. Find the washer’s area, $A$
      1. Find the area of the outer circle, $A_0$
      2. Find the area of the inner circle, $A_1$
      3. $A = A_0 - A_1$
    2. Find the washer’s volume, $V = A \times d$ (depth)
    3. $m = V \times D$ (density)
  2. Batch mass = $n \times m$

Now, it seems like there’s still another refinement to be made:

To find the area of a circle: $A = \pi r^2$

Each of the reusable steps in our solution that involves creating an abstraction (e.g., talking about total volume rather than the lower-level details of inner/outer radii and depth) is a candidates to be a function, even if a simple one. Some examples of function declarations that we talked about in class include:

/**
 * Calculates the volume of a washer-shaped object.
 *
 * @param     r1     radius of the washer
 * @param     r2     radius of the washer's hole [same linear unit as r1]
 *
 * @pre r2 > 0, r2 < r1
 *
 * @returns   total volume [same linear unit cubed]
 */
double washerVolume(double r1, double r2, double depth);

/**
 * Calculates the area of a circle.
 *
 * @param   radius      radius of the circle @pre >= 0
 *
 * @returns the area of the circle [units of radius squared]
 */
double circleArea(double radius);

Exercise: write definitions for these and whatever other functions are required to implement a complete solution to the above problem.

Delegation

This is an exercise in delegation. Each function delegates most of its work to an assistant function. All the inputs (arguments) needed to carry out the whole problem are sent to the topmost function. It can simply passes four of its parameters over to a washerMass function, then multiplies the mass returned by that function by the count in order to calculate the mass of the entire batch. This is a very simple problem and the amount of delegation involved is fairly extreme in that, once each function delegates, it has very little left to do. Nevertheless, it illustrates the principle very well.

In practice, functions run from one line to hundreds of lines in length, with many functions being fewer than 20 lines. Once functions exceed 50–100 lines, it can be a sign that there are opportunities to break it into smaller, more reusable, pieces.

Visibility

We have previously talked about variable scope, but what do we do when there are multiple variables in scope that have the same name? For example, in the following code:

int x;

void foo(int y)
{
	if (y > 0)
	{
		int x = 17;

		while (x > y)
		{
			int z = y / 2;
			x -= z;
		}
	}
}

which x variable is referenced by the statement x -= z? The answer is, when we have a choice of declarations that we might be referring to, we choose the most local one.

Exercises

  1. Write a function that will convert 2D cartesian co-ordinates (i.e., $(x,y)$ co-ordinates) into polar co-ordinates (i.e., $(r,\theta)$).
  2. Write a function that will compute the roots of a quadratic expression, dealing with all special cases (i.e., $a=0$ and $b^2 < 4ac$).
  3. Using getNumberFromUser() from the examples page, write a function that will input an $(x,y,z)$ co-ordinate from a user and make it available to the function that called it.
License: CC BY-NC-SA

(c) 2009–2016 Michael Bruce-Lockhart, Theo Norvell, Dennis Peters and Jonathan Anderson. Licensed under a Creative Commons Attribution–Noncommercial–Share-Alike 2.5 Canada License. Permissions beyond the scope of this license may be available at theteachingmachine.org.