Functions with Numbers
A tax accountant does not rederive the tax formula every time a client walks in. They work it out once, give it a name (“standard deduction calculation”), and apply it to each client’s numbers. Similarly in programming, we often find ourselves writing the same computation in multiple places. In the control flow section, we wrote loops that computed digit sums, counted even numbers, and accumulated products. Each time we needed one of those computations, we wrote the loop from scratch.
We solved this kind of problem in Chapter 1. Functions let us write a computation once, give it a name, and use that name everywhere. The difference is that boolean functions were short. A single expression handled most cases. Numeric functions often need loops, local variables, and multiple statements. The function bodies get longer, but the mechanics are the same.
Single-Expression Functions
We start with functions whose bodies fit on one line.
Func<int, int> Twice = n => n * 2;Translation: “Define a function named Twice that takes an integer n and returns n multiplied by 2.”
The syntax is identical to Chapter 1. Func<int, int> is the type: one integer in, one integer out. Twice is the name. n is the parameter. n * 2 is the returned expression. The same pattern holds from Chapter 1: types list in order, inputs first, output last.
┌─────────────────┐
n ───►│ n * 2 │───► result
(5) │ │ (10)
└─────────────────┘
Twice
An integer enters on the left. The function multiplies it by 2. The result emerges on the right.
Not every function returns the same type it takes in. IsEven takes an integer and returns a boolean:
Func<int, bool> IsEven = n => n % 2 == 0;Translation: “Define a function named IsEven that takes an integer n and returns whether the remainder of n divided by 2 equals 0.”
Func<int, bool> means one integer in, one boolean out. The input and output types do not have to match.
Functions can also take multiple inputs. Add takes two integers and returns their sum:
Func<int, int, int> Add = (a, b) => a + b;Translation: “Define a function named Add that takes two integers a and b and returns the result of a plus b.”
Func<int, int, int> means two integers in, one integer out. As always, the last type is the output.
The grade classifier we wrote in the control flow section took a score and produced a letter. That function has the shape Func<int, string>: one integer in, one string out. We will write it as a named function shortly.
Try it yourself.
Translate this function definition to English:
Func<int, int> Square = n => n * n;Reveal answer
“Define a function named Square that takes an integer n and returns n multiplied by n.”
If your answer differed, note what you missed before continuing.
Try it yourself.
Write a function definition from this description:
Define a function named IsNegative that takes an integer n and returns whether n is less than 0.
Reveal answer
Func<int, bool> IsNegative = n => n < 0;Self-correct against the model above.
We can also use switch expressions inside a function body:
Func<int, string> Classify = n => n switch
{
> 0 => "positive",
< 0 => "negative",
_ => "zero"
};Translation: “Define a function named Classify that takes an integer n and returns ‘positive’ where n is greater than 0, ‘negative’ where n is less than 0, and ‘zero’ otherwise.”
This connects directly to what we covered with switch expressions. The switch expression produces a value from a pattern match. Wrapping it in a function gives it a name.
Try it yourself.
Write a function named LetterGrade that takes an integer score and returns a string: “A” for 90 and above, “B” for 80 and above, “C” for 70 and above, “D” for 60 and above, and “F” otherwise.
Reveal answer
Func<int, string> LetterGrade = score => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
>= 60 => "D",
_ => "F"
};Self-correct against the model above.
How a Function Call Executes
We defined functions and called them in Chapter 1, but the function bodies were short. Not(true) evaluated !true and returned false. Four steps, done.
Numeric functions can contain loops, local variables, and multiple lines of code. To trace these, we need a clearer picture of what happens when a function is called.
Here is a simple example:
Func<int, int> Twice = n => n * 2;
int x = 3;
int result = Twice(x);When the program reaches Twice(x), here is what happens:
1. Evaluate x (get 3).
2. Call Twice with the value 3.
Inside Twice, bind 3 to n.
Evaluate n * 2 → 6.
Return 6.
3. Back where Twice was called, bind 6 to result.
The calling code pauses at the function call. Execution moves to the function body: the function binds the argument to its parameter, runs its body, and returns the result. Then execution continues where the call happened.
The function has its own variable, n, that exists only while the function runs. It does not affect x in the calling code. After the function returns, n is destroyed.
We will use this format throughout the section. The indentation shows when execution is inside a function. “Inside [function]” marks entry into the function body. “Back where [function] was called” marks the return.
Try it yourself.
Trace the execution of this code:
Func<int, bool> IsEven = n => n % 2 == 0;
int value = 7;
bool even = IsEven(value);Write the step-by-step trace, then check.
Reveal answer
- Bind 7 to value
- Evaluate value (get 7)
- Call IsEven with 7
- Inside IsEven, bind 7 to n
- Evaluate n % 2 0 → 7 % 2 0 → 1 == 0 → false
- Return false
- Back where IsEven was called, bind false to even
If your answer differed, note what you missed before continuing.
Multi-Statement Function Bodies
In Chapter 1, multi-statement bodies appeared when we used branching to build boolean operations. Curly braces created a scope, and return ended the function with a value.
Numeric functions use the same syntax, but now the bodies contain loops and local variables.
In the control flow section, we wrote this loop to compute the sum of a number’s digits:
int n = 7364;
int digitSum = 0;
while (n > 0)
{
digitSum += n % 10;
n /= 10;
}
Console.WriteLine(digitSum);This works for 7364, but what if we need the digit sum of a different number? We wrap the loop in a function:
Func<int, int> SumDigits = n =>
{
int digitSum = 0;
while (n > 0)
{
digitSum += n % 10;
n /= 10;
}
return digitSum;
};Translation: “Define a function named SumDigits that takes an integer n and returns the sum of its digits.”
The function body is the same loop we wrote earlier, now enclosed in curly braces with a return statement at the end. The parameter n replaces the hardcoded value. The local variable digitSum exists only inside the function body.
To call it:
int total = SumDigits(7364);Translation: “Call SumDigits with the value 7364 and bind the returned value to an integer variable named total.”
Let’s trace this call:
- Call SumDigits with 7364
- Inside SumDigits, bind 7364 to n
- Create digitSum and bind 0 to it
- Execute the loop:
| iteration | n (start) | n % 10 | digitSum | n /= 10 |
|---|---|---|---|---|
| 1 | 7364 | 4 | 4 | 736 |
| 2 | 736 | 6 | 10 | 73 |
| 3 | 73 | 3 | 13 | 7 |
| 4 | 7 | 7 | 20 | 0 |
- n is 0, loop exits
- Return 20
- Back where SumDigits was called, bind 20 to total
The iteration table is the same format we used for loops. The new piece is the framing around it: we enter the function, run the loop inside it, and return when it finishes.
Try it yourself.
Write a function named Factorial that takes an integer n and returns the factorial of n. The factorial of some number n, written as , is the product of all the numbers from 1 to n, or . So would be equal to .
Use a loop with an accumulator. Start the accumulator at 1 (the identity for multiplication).
Reveal answer
Func<int, int> Factorial = n =>
{
int product = 1;
int i = 1;
while (i <= n)
{
product *= i;
i++;
}
return product;
};Translation: “Define a function named Factorial that takes an integer n and returns the factorial of n.”
Self-correct against the model above.
Try it yourself.
Write a function named CountEvensUpTo that takes an integer n and returns the count of even numbers from 1 through n. For example, CountEvensUpTo(10) returns 5.
Reveal answer
Func<int, int> CountEvensUpTo = n =>
{
int count = 0;
int i = 1;
while (i <= n)
{
if (i % 2 == 0)
{
count++;
}
i++;
}
return count;
};For n = 10:
| iteration | i | i % 2 == 0 | count |
|---|---|---|---|
| 1 | 1 | false | 0 |
| 2 | 2 | true | 1 |
| 3 | 3 | false | 1 |
| 4 | 4 | true | 2 |
| 5 | 5 | false | 2 |
| 6 | 6 | true | 3 |
| 7 | 7 | false | 3 |
| 8 | 8 | true | 4 |
| 9 | 9 | false | 4 |
| 10 | 10 | true | 5 |
Self-correct against the model above.
Functions Calling Functions
Functions can call other functions. This lets us build complex computations from simpler ones.
Suppose we want the average value of a number’s digits. The digit sum of 7364 is 20, and it has 4 digits, so the average is 20 / 4 = 5. We already have SumDigits. We need CountDigits:
Func<int, int> CountDigits = n =>
{
int count = 0;
while (n > 0)
{
count++;
n /= 10;
}
return count;
};Now AverageDigit calls both:
Func<int, int> AverageDigit = n =>
{
int sum = SumDigits(n);
int count = CountDigits(n);
return sum / count;
};Translation: “Define a function named AverageDigit that takes an integer n and returns the sum of its digits divided by the number of digits.”
Let’s trace AverageDigit(7364):
1. Call AverageDigit with 7364.
Inside AverageDigit, bind 7364 to n.
2. Call SumDigits with 7364.
Inside SumDigits, bind 7364 to n.
[Loop executes: 4 iterations, digitSum accumulates to 20]
Return 20.
Back in AverageDigit, bind 20 to sum.
3. Call CountDigits with 7364.
Inside CountDigits, bind 7364 to n.
[Loop executes: 4 iterations, count accumulates to 4]
Return 4.
Back in AverageDigit, bind 4 to count.
4. Evaluate sum / count → 20 / 4 → 5.
Return 5.
Back where AverageDigit was called, bind 5 to result.
Each function call moves execution to that function’s body. When that function returns, execution picks up where the call happened. When AverageDigit calls SumDigits, execution moves to SumDigits. When SumDigits returns, execution continues inside AverageDigit.
Notice that SumDigits has its own n, and AverageDigit has its own n. These are separate variables. Each function gets its own copy of its parameters. Changing n inside SumDigits (the loop divides it by 10 each iteration) does not affect n inside AverageDigit. This is value type behavior, the same principle from Chapter 1.
Try it yourself.
Write a function named SumSquares that takes an integer n and returns the sum of the squares from 1 to n. For example, SumSquares(3) returns 1 + 4 + 9 = 14.
First write a function named Square that takes an integer and returns its square. Then write SumSquares using Square inside a loop.
Reveal answer
Func<int, int> Square = n => n * n;
Func<int, int> SumSquares = n =>
{
int sum = 0;
int i = 1;
while (i <= n)
{
sum += Square(i);
i++;
}
return sum;
};For n = 3:
| iteration | i | Square(i) | sum |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 2 | 4 | 5 |
| 3 | 3 | 9 | 14 |
Check your answer against the model.
Recursion
We have built every computation so far with loops. Initialize a variable, repeat some steps, return the result. Loops describe how to build an answer, one step at a time.
Mathematics often takes a different approach. Instead of describing steps, it defines an answer in terms of a smaller version of the same problem. Consider the factorial function. In math, it is often written as a piecewise definition:
factorial(n) = 1 if n ≤ 1
factorial(n) = n × factorial(n - 1) otherwise
Two cases. If n is 1 or less, the answer is just 1. Otherwise, the answer is n times the factorial of (n - 1).
Let’s compute factorial(4) by hand, following this definition:
factorial(4) = 4 × factorial(3) (4 is not ≤ 1, so use the second case)
= 4 × 3 × factorial(2) (3 is not ≤ 1, second case again)
= 4 × 3 × 2 × factorial(1) (2 is not ≤ 1, second case again)
= 4 × 3 × 2 × 1 (1 is ≤ 1, so the answer is 1)
= 24
Each step asks: which case applies? If n is still greater than 1, we replace factorial(n) with n × factorial(n - 1) and continue. When n reaches 1, we stop.
The first case, where we stop and return a value directly, is the base case. The second case, where we use a smaller version of the same problem, is the recursive case. Every piecewise definition like this needs both. The base case provides a concrete answer. The recursive case breaks the problem down until it reaches the base case.
Definition. Recursion is when a function computes its result by calling itself with smaller inputs until it reaches a base case.
Definition. A base case is a condition under which a recursive function returns a value without calling itself.
Definition. A recursive case is the branch where the function calls itself with an input closer to a base case and uses the returned value to compute its result.
We can write this piecewise definition directly in C#:
Func<int, int> Factorial = null;
Factorial = n =>
{
if (n <= 1) return 1;
return n * Factorial(n - 1);
};Translation: “Define a function named Factorial that takes an integer n and returns the factorial of n. It has a base case: if n is less than or equal to 1, return 1. It has a recursive case: return n multiplied by the result of calling Factorial with n minus 1.”
The code mirrors the piecewise math. The if statement checks for the base case. If it applies, the function returns 1. Otherwise, the function calls itself with an input closer to the base case and multiplies.
The first line, Func<int, int> Factorial = null;, looks unusual. For a function to refer to itself by name, that name must already exist. We create the variable first, then bind the function to it on the next line. Otherwise, dotnet will crash when you run it.
Let’s trace Factorial(4) the way we trace any function call, using the Inside/Back format:
Call Factorial with 4.
Inside Factorial, bind 4 to n.
n <= 1 is false.
Evaluate 4 * Factorial(3).
Call Factorial with 3.
Inside Factorial, bind 3 to n.
n <= 1 is false.
Evaluate 3 * Factorial(2).
Call Factorial with 2.
Inside Factorial, bind 2 to n.
n <= 1 is false.
Evaluate 2 * Factorial(1).
Call Factorial with 1.
Inside Factorial, bind 1 to n.
n <= 1 is true.
Return 1.
Back in Factorial: 2 * 1 = 2. Return 2.
Back in Factorial: 3 * 2 = 6. Return 6.
Back in Factorial: 4 * 6 = 24. Return 24.
Back where Factorial was called, bind 24 to result.
Each call waits for the one below it to finish. Factorial(4) cannot compute 4 * Factorial(3) until Factorial(3) returns. This continues until Factorial(1) hits the base case and returns 1. Then the results flow back up.
Look at both versions side by side:
Loop:
Func<int, int> Factorial = n =>
{
int product = 1;
int i = 1;
while (i <= n)
{
product *= i;
i++;
}
return product;
};Recursion:
Func<int, int> Factorial = null;
Factorial = n =>
{
if (n <= 1) return 1;
return n * Factorial(n - 1);
};The loop version needs an accumulator, a counter, and a while condition. The recursive version has two lines that mirror the piecewise math directly. When a problem has a natural recursive structure, as factorial does, the recursive code is often simpler.
Try it yourself.
Trace Factorial(3) step by step. Write the full chain of calls, then check.
Reveal answer
Call Factorial with 3.
Inside Factorial, bind 3 to n.
n <= 1 is false.
Evaluate 3 * Factorial(2).
Call Factorial with 2.
Inside Factorial, bind 2 to n.
n <= 1 is false.
Evaluate 2 * Factorial(1).
Call Factorial with 1.
Inside Factorial, bind 1 to n.
n <= 1 is true.
Return 1.
Back in Factorial: 2 * 1 = 2. Return 2.
Back in Factorial: 3 * 2 = 6. Return 6.
Back where Factorial was called, bind 6 to result.
If your answer differed, note what you missed before continuing.
Here is a second example. We wrote SumTo as a loop earlier: sum the integers from 1 through n. We can also write it recursively:
Func<int, int> SumTo = null;
SumTo = n =>
{
if (n <= 0) return 0;
return n + SumTo(n - 1);
};Translation: “Define a function named SumTo that takes an integer n and returns the sum of integers from 1 to n. It has a base case: if n is less than or equal to 0, return 0. It has a recursive case: return n plus the result of calling SumTo with n minus 1.”
The logic: the sum from 1 to n equals n plus the sum from 1 to (n - 1). The sum from 1 to 0 is 0. Each call reduces n by 1, moving toward the base case.
Let’s verify with SumTo(4):
SumTo(4) = 4 + SumTo(3)
= 4 + 3 + SumTo(2)
= 4 + 3 + 2 + SumTo(1)
= 4 + 3 + 2 + 1 + SumTo(0)
= 4 + 3 + 2 + 1 + 0
= 10
This shortened form is useful for checking that the recursion produces the correct result. The full trace with “Inside / Back where [function] was called” framing shows the execution mechanism. The shortened form shows the mathematical structure.
Try it yourself.
Write a recursive function named CountDigits that takes an integer n and returns the number of digits in n. For example, CountDigits(7364) returns 4.
Hint: a single-digit number (less than 10) has 1 digit. A larger number has 1 plus the number of digits in n / 10.
Reveal answer
Func<int, int> CountDigits = null;
CountDigits = n =>
{
if (n < 10) return 1;
return 1 + CountDigits(n / 10);
};Translation: “Define a function named CountDigits that takes an integer n and returns the number of digits in n. It has a base case: if n is less than 10, return 1. It has a recursive case: return 1 plus the result of calling CountDigits with n divided by 10.”
Verification:
CountDigits(7364) = 1 + CountDigits(736)
= 1 + 1 + CountDigits(73)
= 1 + 1 + 1 + CountDigits(7)
= 1 + 1 + 1 + 1
= 4
Self-correct against the model above.
Loops vs. Recursion
Loops and recursion are two strategies for building computations. A loop says how to build the answer step by step. A recursive definition says what the answer is in terms of a smaller version of itself. This is the same kind of choice we saw in Chapter 1 with composition vs. branching: two approaches, neither always better. For some problems, a loop is clearer. For others, the recursive definition matches the problem’s structure more naturally. Practice writing both.
Try it yourself.
Write a function named Power that takes two integers, base and exponent, and returns base raised to the exponent. Write it twice: once with a loop, once with recursion.
Hint for the loop: start an accumulator at 1, multiply by base for each step from 1 to exponent.
Hint for recursion: base to the power of 0 is 1. Base to the power of n is base times base to the power of (n - 1).
Reveal answer
Loop version:
Func<int, int, int> Power = (b, exp) =>
{
int result = 1;
int i = 1;
while (i <= exp)
{
result *= b;
i++;
}
return result;
};Recursive version:
Func<int, int, int> Power = null;
Power = (b, exp) =>
{
if (exp == 0) return 1;
return b * Power(b, exp - 1);
};Verification of the recursive version with Power(2, 4):
Power(2, 4) = 2 * Power(2, 3)
= 2 * 2 * Power(2, 2)
= 2 * 2 * 2 * Power(2, 1)
= 2 * 2 * 2 * 2 * Power(2, 0)
= 2 * 2 * 2 * 2 * 1
= 16
Check your answers against the models.
Review
Before continuing, test yourself on what you’ve learned. Use the protocol from Chapter 0: attempt each exercise from memory, then search this section to check your answers, then note what you missed.
Part 1: Definitions
Write the definitions from memory, then find them in this section to check.
- What is recursion?
- What is a base case?
- What is a recursive case?
Also recall from Chapter 1:
- What is a function?
- What is a parameter?
- What is an argument?
If any of your answers differed from the definitions, note what you missed and write the corrected version. Items 1-3 can be checked in this section. Items 4-6 can be checked in the functions section of Chapter 1.
Part 2: Type Signatures
Write the meaning of each type signature.
Func<int, int>Func<int, bool>Func<int, string>Func<int, int, int>
Check your answers against the explanations in this section.
Part 3: Translations
Translate each piece of code to English.
-
Func<int, int> Triple = n => n * 3; -
Func<int, bool> IsDivisibleBy3 = n => n % 3 == 0; -
int result = SumDigits(n); -
Func<int, int> SumTo = null; SumTo = n => { if (n <= 0) return 0; return n + SumTo(n - 1); };
Check your translations against the patterns shown in this section.
Part 4: Writing Code
Write C# code for each description.
-
Define a function named Cube that takes an integer n and returns n multiplied by n multiplied by n.
-
Define a function named SumOddsUpTo that takes an integer n and returns the sum of all odd numbers from 1 through n. Use a loop with a filter.
-
Define a recursive function named SumDigits that takes an integer n and returns the sum of its digits. It has a base case: if n is less than 10, return n. It has a recursive case: return (n modulo 10) plus the result of calling SumDigits with n divided by 10.
Check your code against the examples in this section.
Part 5: Tracing
- Trace the execution of this code. Show the step-by-step flow including what happens inside the function.
Func<int, int> Twice = n => n * 2;
int x = 5;
int y = Twice(x);-
Trace
SumTo(3)using the full trace format (Inside / Back where [function] was called). -
Trace
CountDigits(84)using the shortened form.
Check your traces against the examples in this section.
Part 6: Complete Programs
Write a complete program that asks the user for a positive integer and displays the sum of its digits. Use a function named SumDigits. If the user enters a non-positive number, ask again until they provide a valid one.
Check your program by tracing through it with sample inputs.
You now know how to define your own numeric computations. You can package loops and logic into named functions, trace how execution moves between calling code and function bodies, call functions from inside other functions, and use recursion as an alternative to loops. In the next section, we’ll group related numeric data into custom types.
Previous: Section 3 - Control Flow
Next: Section 5 - Custom Types