Learning Expressions and Functions by Inventing Them

You won't be taught — you will discover. Every step builds on the last. Trust the process.

Part 1: Pulling Numbers Apart

How Is a Number Built?

Take the number 2784. In your head, break it into place values:

2784 = 2000 + 700 + 80 + 4
     = 2*1000 + 7*100 + 8*10 + 4*1

Python has two operators that are perfect for taking numbers apart like this:

  • % — the modulo operator, gives the remainder of a division
  • //integer division, gives the result of a division with the remainder thrown away

Before coding, answer these questions:

  1. What do you expect 2784 % 10 to be?
  2. What do you expect 2784 // 10 to be?
  3. Which of the two — % or // — do you expect gives you the last digit of a number? Which gives you the number with the last digit chopped off?
  4. Will your answer to #3 still hold for 50, for 7, and for 100? Reason about it before running anything.

Extract the Last Digit

Write a function last_digit(n) that returns the last digit of a non-negative integer n.

Example:

last_digit(2784)  # Output: 4
last_digit(7)      # Output: 7
last_digit(100)    # Output: 0
last_digit(0)      # Output: 0

Remove the Last Digit

Write a function remove_last_digit(n) that returns n with its last digit chopped off.

Example:

remove_last_digit(2784)  # Output: 278
remove_last_digit(7)     # Output: 0
remove_last_digit(100)   # Output: 10

Think about it: what should remove_last_digit(0) return? Decide before you run it, then check.

Verify the Round Trip

Here's a sanity check: for any n, you should be able to rebuild it from remove_last_digit(n) and last_digit(n).

Tasks:

  1. For n = 2784, compute remove_last_digit(n) * 10 + last_digit(n) by hand (using the values from Exercises 1.2 and 1.3). Does it equal n?
  2. Write a function check_roundtrip(n) that returns True if remove_last_digit(n) * 10 + last_digit(n) == n, False otherwise.
  3. Test it on at least 5 different numbers, including 0 and a single-digit number.

Part 2: A Function Built from a Formula — Simple Interest

The Formula

When you deposit or borrow money, it grows over time due to interest. The formula for Simple Interest is:

Interest = (Principal * Rate * Time) / 100

where:

  • Principal — the amount deposited or borrowed
  • Rate — the interest rate per year, as a percentage
  • Time — the number of years

Before coding, answer these questions:

  1. If you deposit 1000 at a rate of 5% for 2 years, what's the interest, by hand?
  2. What happens to the interest if Time = 0? Does that match your intuition about lending money for zero years?

Write `calculate_interest`

Write a function calculate_interest(principal, rate, time) that returns the simple interest using the formula above.

Example:

calculate_interest(1000, 5, 2)    # Output: 100.0
calculate_interest(1500, 4.3, 3)  # Output: 193.5
calculate_interest(500, 10, 0)    # Output: 0.0

Explore the Formula

Tasks:

  1. Fix principal = 1000 and rate = 5. Compute the interest for time = 1, 2, 3, ..., 10. What pattern do you notice in how the interest grows as time increases by 1 each time?
  2. Now fix principal = 1000 and time = 1. Compute the interest for rate = 1, 2, ..., 10. Is the pattern the same kind of growth as in #1?
  3. In one sentence, describe what you'd call this kind of growth (hint: it's the same shape as the line y = m*x + c you may already know).

Part 3: Functions with Several Arguments — Distance Formulas

The Hypotenuse

For a right triangle with legs base and height, the Pythagorean theorem says:

hypotenuse**2 = base**2 + height**2

Before coding: work out by hand — if base = 3 and height = 4, what is the hypotenuse?

Write `calculate_hypotenuse`

Write calculate_hypotenuse(base, height) that returns the hypotenuse using the formula above. You can use ** 0.5 or math.sqrt to take the square root.

Example:

calculate_hypotenuse(3, 4)    # Output: 5.0
calculate_hypotenuse(5, 12)   # Output: 13.0
calculate_hypotenuse(0, 0)    # Output: 0.0

Distance Between Two Points (2D)

A point on a flat plane is given by an (x, y) pair. The distance between two points (x1, y1) and (x2, y2) is exactly the hypotenuse of the right triangle formed by their horizontal and vertical separations.

Before coding:

  1. For the points (0, 0) and (3, 4), what are the lengths of the horizontal leg (base) and vertical leg (height) of the right triangle between them?
  2. Using your answer to #1 and the hypotenuse formula from Exercise 3.2, write out — in words or algebra, not code yet — the formula for find_distance_2d(x1, y1, x2, y2).

Your derivation:

  • base = ...
  • height = ...
  • find_distance_2d(x1, y1, x2, y2) = ...

Write `find_distance_2d`

Write find_distance_2d(x1, y1, x2, y2) that returns the distance between the two points. You can use math.sqrt.

Example:

find_distance_2d(0, 0, 3, 4)   # Output: 5.0
find_distance_2d(1, 2, 4, 6)   # Output: 5.0

Extend to 3D

A point in 3D space has three coordinates: (x, y, z).

Before coding: extend your 2D distance formula to three dimensions. What term do you need to add, and why?

Your derivation:

calculate_distance_3d(x1, y1, z1, x2, y2, z2) = ...

Write `calculate_distance_3d`

Write calculate_distance_3d(x1, y1, z1, x2, y2, z2) that returns the distance between the two 3D points.

Example:

calculate_distance_3d(0, 0, 0, 1, 1, 1)  # Output: 1.7320508075688772
calculate_distance_3d(2, 3, 5, 2, 3, 5)  # Output: 0.0
calculate_distance_3d(1, 2, 3, 4, 6, 9)  # Output: 7.810249675906654

A Different Kind of Distance: Manhattan

Imagine a taxi that can only drive along a grid of streets — no diagonal shortcuts. The distance it travels between two points is called the Manhattan distance.

Before coding:

  1. From (2, 3) to (5, 1), how far would the taxi have to drive if it can only move horizontally and vertically (never diagonally)?
  2. Write the formula using absolute values: manhattan_distance(x1, y1, x2, y2) = ?. (Python's absolute value function is abs(...).)

Your derivation:

manhattan_distance(x1, y1, x2, y2) = ...

Write `manhattan_distance`

Example:

manhattan_distance(2, 3, 5, 1)    # Output: 5
manhattan_distance(0, 0, 0, 0)    # Output: 0
manhattan_distance(-1, -1, 1, 1)  # Output: 4

Compare the Two Distances

Tasks:

  1. For the points (0, 0) and (3, 4), compute both find_distance_2d and manhattan_distance. Which is bigger?
  2. Will the Manhattan distance ever be smaller than the Euclidean distance, for the same two points? Reason about it, or try a few pairs of points to support your answer.
  3. Find a pair of points for which the two distances come out equal. What do those points have in common?

Distance Metric Intuition

A delivery drone flies in a straight line from point A to point B, but a delivery truck must follow streets laid out in a grid. Which distance formula should you use to estimate each vehicle's travel distance?

A. Euclidean for both

B. Manhattan for both

C. Euclidean for the drone, Manhattan for the truck

D. Manhattan for the drone, Euclidean for the truck

Part 4: Functions That Model a Line

Predicting `y` from `x`

A straight line in 2D can be written as:

y = m * x + c
  • m — the slope (steepness)
  • c — the intercept (where the line crosses the y-axis)

Before coding: if m = 2, c = 3, x = 4, what is y, by hand?

Write `predict`

Write predict(m, c, x) that returns y = m * x + c.

Example:

predict(2, 3, 4)    # Output: 11
predict(-1, 5, 2)   # Output: 3

Plot the Line

Use the plotting helper below to see your line. It calls your own predict function under the hood.

Don't worry about understanding the plotting code — we will study it later in class. For now, just run it and use it.

Try plotting a couple of lines:

plot_line(2, 3)
plot_line(-1, 5)

Working Backwards: From Two Points to a Line

So far you've gone from (m, c, x) to y. Now go the other direction: given two points that lie on a line, figure out the line's m and c.

Before coding:

  1. Recall the definition of slope: how much y changes per one-unit change in x. If you know two points (x1, y1) and (x2, y2) on the line, write a formula for m in terms of these four numbers.
  2. Once you know m, how can you find c using just one of the two points and the line equation y = m*x + c? (Hint: rearrange the equation to solve for c.)
  3. By hand: for the points (1, 2) and (3, 6), what are m and c?

Your derivation:

  • m = ...
  • c = ...
  • For (1, 2) and (3, 6): m = ..., c = ...

Write `fit`

Write fit(x1, y1, x2, y2) that returns a tuple (m, c) — the slope and intercept of the line through the two points.

Example:

fit(1, 2, 3, 6)    # Output: (2.0, 0.0)
fit(0, 5, 2, 9)    # Output: (2.0, 5.0)

Think about it: what happens if x1 == x2? The line would be vertical, and m would be undefined (division by zero). You don't need to fix this — just notice it, and let your function do whatever Python naturally does in that case.

Round Trip: `fit` Then `predict`

Tasks:

  1. Use fit to recover (m, c) from the points (1, 2) and (3, 6).
  2. Use predict with that (m, c) to compute y at x = 1 and x = 3. Do you get back 2 and 6?
  3. Try a third value, x = 5. According to your fitted line, what should y be?
  4. Plot the line together with the two original points using plot_line(m, c, extra_points=[(1, 2), (3, 6)]) to check visually.

Part 5: Functions That Take Other Functions — The Derivative

Rate of Change, Without Calculus

The derivative of a function at a point tells you how fast the function's output is changing there — its instantaneous slope.

You don't need calculus rules to approximate this. Recall Exercise 4.4: the slope between two points is (y2 - y1) / (x2 - x1).

Idea: pick two points on the function's curve that are very close together, and compute the slope between them.

Before coding:

  1. For f(x) = x * x, compute f(3) and f(3.0001) by hand.
  2. Treat (3, f(3)) and (3.0001, f(3.0001)) as two points on a line, and use the slope formula from Exercise 4.4 to compute the slope between them. This is your approximate derivative of f at x=3.
  3. The true derivative of x**2 is 2*x. At x=3 that's 6. How close is your approximation?

Your derivation:

  • f(3) = ...
  • f(3.0001) = ...
  • approximate slope = ...
  • compared to true derivative (6): ...

Generalize: `approx_derivative`

Formalize what you just did. This is called the forward difference method:

$$f'(x) \approx \frac{f(x + h) - f(x)}{h}$$

where h is a small number.

Write a function approx_derivative(func, x, h) that:

  • takes a function func of one argument,
  • a point x,
  • a small step h,
  • and returns the approximate derivative of func at x.

Example:

def square(x):
    return x * x

approx_derivative(square, 3, 0.0001)   # Output: approx 6

Test on More Functions

Tasks:

  1. Try approx_derivative on cube(x) = x*x*x at x=2. The true derivative of x**3 is 3*x**2 — at x=2 that's 12. How close are you?
  2. Try it on math.sin at x=0. The true derivative of sin(x) at 0 is cos(0) = 1.
  3. What happens to the accuracy of your approximation as h gets smaller — try h = 0.1, 0.01, 0.0001, 0.0000001? What about when h gets too small (like 1e-15)? Can you explain what you observe?

The Tangent Line's Intercept

A function's derivative at a point is the slope of the tangent line — the line that just barely touches the curve there.

Recall from Part 4: a line is y = m*x + c. If you know the slope m (your derivative) and one point on the line, (x0, func(x0)), you can find c using the exact same idea as in Exercise 4.4.

Tasks:

  1. Write tangent_line_intercept(func, x0, h) that:
    • computes the slope using approx_derivative(func, x0, h),
    • then computes the intercept c such that the tangent line passes through (x0, func(x0)).
  2. Test it on square at x0 = 3.

Plot the Tangent Line

Use the plotting helper below to see the function and its tangent line together. Pass in the slope and intercept you just computed.

Try it:

x0 = 3
h = 0.0001
m = approx_derivative(square, x0, h)
c = tangent_line_intercept(square, x0, h)

plot_function_with_tangent(square, x0, m, c, x_range=(-1, 6))

What the Derivative Tells You

You computed approx_derivative(f, 3) and got a value of -2. What does this tell you about the function f at x = 3?

A. f(3) = -2

B. The function is decreasing at x = 3 and its tangent line slopes downward

C. The function reaches its minimum at x = 3

D. The function crosses zero somewhere near x = 3

Part 6: Bonus Challenges

If you've made it here, you've built a small toolkit of expressions and functions from scratch. These challenges are open-ended — push further if you're curious.

Extract All Digits

Using last_digit and remove_last_digit from Part 1 (plus a loop), write all_digits(n) that returns a list of n's digits, in order.

all_digits(2784)  # Output: [2, 7, 8, 4]

Distance in N Dimensions

find_distance_2d and calculate_distance_3d look almost identical except for how many coordinates they take. Generalize: write distance_nd(point1, point2) where point1 and point2 are lists of any length (same length as each other), and return the Euclidean distance between them.

distance_nd([0, 0], [3, 4])               # Output: 5.0
distance_nd([0, 0, 0], [1, 1, 1])         # Output: 1.7320508075688772
distance_nd([1, 2, 3, 4], [4, 6, 8, 4])  # 4D!

A More Accurate Derivative

The forward difference method looks ahead of x only. The central difference method looks on both sides:

$$f'(x) \approx \frac{f(x + h) - f(x - h)}{2h}$$

Write central_derivative(func, x, h) and compare its accuracy against approx_derivative for cube at x=2 (true answer: 12), using the same values of h you tried in Exercise 5.3. Which method gets closer, faster, as h shrinks?

The Second Derivative

The second derivative tells you how the slope itself is changing. You already have a function that computes a derivative — what happens if you feed approx_derivative a function that is itself a derivative?

Write second_derivative(func, x, h) that approximates $f''(x)$ by applying approx_derivative twice. Test it on cube(x) = x**3, whose second derivative is 6*x — at x=2 that's 12.

Your Own `map`

You've been writing the same loop again and again: go over a list, do something to each element, collect the results. Capture the pattern once and for all.

Write mymapper(func, lst) that returns a new list containing func(x) for each x in lst. Notice what you're doing: passing a function as an argument, just like you did with approx_derivative.

Your Own `reduce`

The other loop you keep writing: combine all the elements into one value (a sum, a product, a maximum...).

Write myreducer(func, lst) where func takes two arguments. Start with the first element, then repeatedly combine the running result with the next element: myreducer(add, [1, 2, 3, 4]) computes add(add(add(1, 2), 3), 4).

Functions That Make Functions

If a function can take a function as input, can it also return one as output?

Write create_power_function(p) that returns a new function: the returned function takes x and computes x ** p. So create_power_function(2) builds a squaring function and create_power_function(3) builds a cubing function — one factory, infinitely many functions.

(The returned function "remembers" p even after the factory has finished running — this is called a closure.)

Part 7: Reflection — What Did You Just Build?

Reflection

Take a moment to answer these questions in your own words.

  1. What's the difference between % and //? When would you reach for each one?
  2. Why does wrapping a formula in a function (like calculate_interest) save you work compared to retyping the formula every time?
  3. What did extending 2D distance to 3D teach you about how formulas generalize?
  4. What's the difference between Euclidean distance and Manhattan distance? When might Manhattan distance be the more meaningful one?
  5. What's the relationship between predict and fit? Which one goes from parameters to data, and which one goes from data to parameters?
  6. In your own words, what does approx_derivative actually compute? Why doesn't making h as tiny as possible always give a better answer?