Coursenotes index | CSC 123 Introduction to Community Action Computing

Function comprehension

We continue with our pattern for understanding: by considering data and allowed operations on that data.

We will focus on two aspects of functions as they relate to operations. First, what are the operations that can be performed on functions? Second, how do functions relate to operations?

There is a broader discussion to be had regarding the question Are functions themselves data? We will revisit that question later this term.

Steps of evaluation

Given the following function definition:

const f = (x: number): number => {
  return (x * x) + 34;
};

What does the following expression evaluate to?

f(2)

Did you determine that it evaluates to 38? If yes, good. If not, that’s ok. Either way, let us explore the steps of evaluation so that we can better understand what happens when a function is called.

Evaluating a function call

Again, consider the following code:

const f = (x: number): number => {
  return (x * x) + 34;
}

f(2)

When working with functions, we will track not only the steps of evaluation but also information that is known based on the code.

For instance, from this code we know the definition of function f and, it turns out, that the call f(2) evaluates to something. We will track the Known information along with our steps of evaluation.

Below is a table that tracks the evaluation steps for this function call. Step 1 starts us off.

To start with, we don’t have a value for x. x gets a value only once we start evaluating the function call f(2) (Step 2).

At each step, the (sub-)expression or statement that’s currently being evaluated is underlined and highlighted.

Step Under evaluation Known information
1 f(2) const f = (x: number): number => {
 return (x * x) + 34;
}

x = ?

f(2) = ?
2 return (x * x) + 34 const f = (x: number): number => {
 return (x * x) + 34;
}

x = 2

f(2) = ?
3 return (2 * x) + 34 const f = (x: number): number => {
 return (x * x) + 34;
}

x = 2

f(2) = ?
4 return (2 * 2) + 34 const f = (x: number): number => {
 return (x * x) + 34;
}

x = 2

f(2) = ?
5 return 4 + 34 const f = (x: number): number => {
 return (x * x) + 34;
}

x = 2

f(2) = ?
6 return 38 const f = (x: number): number => {
 return (x * x) + 34;
}

x = 2

f(2) = 38

The result of the function call is determined by the return statement. Once the value to be returned is determined, then the result of the function is known (as shown, in bold, in the last row).

Another example

To emphasise the point, let’s work through another example.

const f = (x: number): number => {
    return g(x) + g(x + 1);
}

const g = (y: number): number => {
    return y * (y - 1);
}

f(2)

This call to function f looks very similar to the previous evaluation, and in many respects it is. The steps of evaluation for the function call itself are the same for every function:

Evaluation of f(2)

We begin, as before, with the evaluation of the function call. The complete evaluation is lengthy, so the presentation is split into pieces, but you will soon discover another important reason for this split.

Step Under evaluation Known
1 f(2) const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2)
2 x = 2
return g(x) + g(x + 1)
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = ?
3 x = 2
return g(2) + g(x + 1) | const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = ?
g(2) = ?
 

In step 2, we need to deal with the line g(x) + g(x + 1). Recall that when functions are “called”, they can be dealt with as expressions. This means that the entire line g(x) + g(x + 1) should be evaluated as an expression. We do this by first evaluating each of its sub-expressions, starting with g(x).

The variable x determines the value that is given to this call to function g. The variable itself is not passed to the function. Instead, the variable x is evaluated first to determine its value (as we would do if, say, we were simply evaluating the expression x + 1).

But what comes next?

As demonstrated when evaluating arithmetic expressions (such as x + y), the left operand is evaluated before the right and then the result of the arithmetic can be determined. Similarly, Step 3 must evaluate g(2) as described above.

This is, of course, another function call. This new call must be evaluated before the evaluation of f(2) can complete. Note, as well, the addition of g(2) to the Known information in the table. We know that g(2) evaluates to something—we need to work out what.

Evaluating the nested function call

Let us temporarily suspend the evaluation of the call f(2) and consider the call g(2).

In Step 1 below, all we know is that g(2) evaluates to something. So we need to expand the function call and consider the function body. This means that the parameter now has a value. In Step 2, we have the additional information that y = 2. The evaluation continues like we’ve seen before.

Step Under evaluation Known
1 g(2) const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = ?
2 y = 2
return y * (y - 1)
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = ?
3 y = 2
return 2 * (y - 1)
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = ?
4 y = 2
return 2 * (2 - 1)
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = ?
5 y = 2
return 2 * 1
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = ?
6 y = 2
return 2
const f = (x: number): number => {
 return g(x) + g(x + 1);
}

const g = (y: number): number => {
 return y * (y - 1);
}

f(2) = \<in progress\>
g(2) = 2

Recall that we were in the middle of evaluating g(x) + g(x + 1) in the function f. Since we called f(2), we know that x = 2 inside f. We also now know that g(2) gives us the value 2.

So we have 2 + g(x + 1). We can continue evaluating the rest of this expression, following the same steps that we have outlined. We need to start by evaluating the sub-expression g(x + 1).

Following the steps we’ve learned:

Therefore, at the end of the following code:

const f = (x: number): number => {
  return g(x) + g(x + 1);
}

const g = (y: number): number => {
  return y * (y - 1);
}

f(2)

We can say f(2) gives us the value 8.

That’s….a lot

The level of details in tracking these evaluation steps as just presented can be a bit daunting. This is presented not because you are expected to write such steps explicitly (certainly not frequently) but rather to emphasize the very important individual steps and considerations involved. These are summarized below.