Foundations to FrontierPart II: FoundationsBezhig

C02: Functions, recursion, search, and debugging

5 exercises, each with a complete solution. 1 figure. 1 code listing.

On this page
  1. A. Prerequisites and observable goals
  2. B. The central question: how do we control complexity?
  3. C. Scope and calls
  4. D. Why binary search is about a boundary
  5. E. A worked duplicate case
  6. F. Debugging as a sequence of falsifiable hypotheses
  7. G. Independent practice
  8. H. Complete solutions: C02
  9. I. Retention and readiness

A. Prerequisites and observable goals#

Before

C01: state, expressions, loops, lists, and contracts.

After

Explain function inputs and returns, trace nested calls, construct a terminating recursive definition, and implement a boundary-safe binary search with an explicit invariant.

Level: Foundation to degree core.

B. The central question: how do we control complexity?#

A function creates a boundary around a computation. Its caller supplies arguments; its body uses parameters and local names; its return value supplies a result. Printing a value and returning it are different effects. A program that prints the right answer may still return None to its caller. A useful interface specifies both normal results and failure behavior. The smaller and clearer that contract, the easier it is to test the function independently.

Suppose square(x) returns x*x. Evaluating square(2+3) first computes the argument value 5, then the call computes 25. This resembles mathematical substitution, but a programming function may mutate objects, read a file, or fail. A pure mathematical function assigns a single output to an input within its domain; not every procedure is pure or total. Keep those distinctions visible when a routine interacts with engine state.

C. Scope and calls#

A local name belongs to an invocation, not to every call of a function collectively. In a recursive sum, one invocation waits for the return value of another. Conceptually, each waiting call keeps its parameters, locals, and a continuation—the place to resume. A physical compiler may optimize away some of this structure; the diagram is a semantic model rather than a promised stack-frame layout.

S(0)=0,S(n)=n+S(n−1)(n∈N, n>0).S(0)=0,\qquad S(n)=n+S(n-1)\quad(n\in\N,\ n>0).

For S(3)S(3), substitution gives 3+(2+(1+0))=63+(2+(1+0))=6. The base case is not optional: it supplies a completed result instead of another call. The argument decreases by one until reaching zero. A recursive program accepting negative integers without a rejection rule would not reach this base case. Python also places practical bounds on recursion depth; use iteration for a long linear recurrence. [PY]

D. Why binary search is about a boundary#

Given sorted data a0≤⋯≤an−1a_0\le\cdots\le a_{n-1}, find the first index ii with ai≥ta_i\ge t, or nn if none exists. This is called a lower bound. It handles missing values and duplicates without a special “not found” value that might be confused with index zero.

Maintain a half-open interval [ℓ,r)[\ell,r), initially [0,n)[0,n). All indices below ℓ\ell contain values <t<t; all indices at or above rr contain values ≥t\ge t. When ℓ<r\ell<r, let m=ℓ+⌊(r−ℓ)/2⌋m=\ell+\lfloor(r-\ell)/2\rfloor. If am<ta_m<t, then sortedness makes every index through mm too small, so set ℓ=m+1\ell=m+1. Otherwise mm is a possible answer and every later index is not earlier than it, so set r=mr=m. At termination ℓ=r\ell=r, the invariant identifies the exact boundary.

The arithmetic r−ℓr-\ell is a nonnegative integer that strictly decreases in either branch. That proves termination. Each iteration roughly halves the unresolved range, giving O(log⁡(n+1))O(\log(n+1)) comparisons. This is a comparison count, not a guarantee of logarithmic wall time for an arbitrary sequence with expensive indexed access. A linked list does not provide constant-time indexing. The lab accepts an indexable sorted sequence and does not spend O(n)O(n) verifying sortedness on every call.

E. A worked duplicate case#

For [1,3,3,8] and target 3, initially (ℓ,r)=(0,4)(\ell,r)=(0,4). The midpoint is 2, where the value is 3, so r=2r=2. Now m=1m=1, again value 3, so r=1r=1. Now m=0m=0, value 1 is too small, so ℓ=1\ell=1. Stop at index 1, not index 2. Returning immediately on any equality would violate the first occurrence contract.

For target 9, the search ends at 4. That is a valid insertion position but not a valid existing element index. The caller must not blindly evaluate a[4]. Empty input ends immediately at zero without an indexed read. These boundary behaviors follow from the invariant, not from a pile of unrelated exception cases.

F. Debugging as a sequence of falsifiable hypotheses#

Reproduce a failure with the smallest input that still exhibits it. State expected and actual behavior. Locate the first incorrect state, not merely the final incorrect output. Form one specific hypothesis, change one cause, and add a regression test. For a nonterminating search, a useful hypothesis is that one branch does not shrink the interval when its length is one. For an I/O error, determine whether the file could not be opened, its text could not be decoded, its fields could not be parsed, or its parsed values violated the domain. Those are different failures and deserve different diagnostics.

A traceback identifies the call sequence where an exception propagated. It does not establish which abstraction should handle it. Low-level parsing should report malformed input; a command-line wrapper should convert that report to a nonzero exit status and a readable message. Catching every exception and returning zero hides programmer defects as success.

G. Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

C02.01FSolution

A function prints x*x but has no return statement. What does answer = f(4) bind to answer? How should a reusable numerical function differ from a display wrapper?

C02.02CSolution

Trace S(4)S(4) using the recurrence above. Prove termination on nonnegative integers, and explain why a base case alone is insufficient when the recursive argument is n+1n+1.

C02.03CSolution

Trace lower-bound search for target 4 in [1,3,3,8]. Then show why replacing lo=mid+1 by lo=mid can cause an infinite loop.

C02.04TSolution

A program divides by the number of valid rows after silently ignoring every malformed row. Its output is sometimes a division-by-zero exception. Give a minimal failing file, identify the missing contract, and propose error behavior that does not conceal data quality.

C02.05LabSolution

Implement lower_bound in search_lab.py, using [ℓ,r)[\ell,r) and the stated sorted-input precondition. Handle duplicates, absent targets, and empty sequences. Compare results with Python’s bisect_left on generated sorted lists. Explain why such comparison is a useful test but not your correctness proof.

Optional small hints

C02.03: try a one-element list whose element is below the target. C02.05: when equality occurs, which half could still contain an earlier qualifying index?

H. Complete solutions: C02#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

C02.01Show solution

A visible effect is not a returned value

The function displays 16, but answer is bound to None, the default return value when execution reaches the end of a Python function. Make the computational function return x*x; let a separate caller print the return value. This allows a caller to reuse 16 in arithmetic and a test to compare it with an expected result without intercepting console output.

Back to C02.01
C02.02Show solution

The descent measure

S(4)=4+S(3)=4+3+S(2)=4+3+2+S(1)=4+3+2+1+S(0)=10S(4)=4+S(3)=4+3+S(2)=4+3+2+S(1)=4+3+2+1+S(0)=10. For every positive integer nn, the next argument n−1n-1 is smaller and nonnegative. A strictly decreasing nonnegative integer cannot decrease forever, so the base case is reached. With n+1n+1, the argument increases and never reaches zero from a positive starting value. A base case that is never reached does not prove termination. With machine limits, recursion can also fail before a mathematically terminating computation finishes; operational resource limits are separate obligations.

Back to C02.02
C02.03Show solution

The excluded midpoint must really be excluded

For target 4, (0,4)(0,4) gives midpoint 2 and value 3, so ℓ=3\ell=3. Then (3,4)(3,4) gives midpoint 3 and value 8, so r=3r=3. Return 3. Under the faulty update on [1] and target 2, (0,1)(0,1) gives midpoint zero. Setting ℓ=0\ell=0 changes nothing, so the same branch repeats forever. Since the midpoint was proved too small, it must be excluded by adding one.

Back to C02.03
C02.04Show solution

Make the missing-data state explicit

A file containing only a header and one invalid measurement such as time,latency followed by 0,not-a-number leaves zero valid rows. The program lacks a policy for invalid records and an empty valid dataset. A strict tool can reject the file with a line number and nonzero exit status. A permissive tool must report the rejected count, require at least one valid row, and identify the changed population being summarized. Returning a fabricated zero average would make missing observations look like excellent performance. J01 adopts the strict policy for a bounded learning project.

Back to C02.04
C02.05Show solution

Reference implementation

code/solutions/search_lab.pyPython
"""C02.05: lower bound on a sorted sequence; sortedness is a precondition."""
from collections.abc import Sequence

def lower_bound(values: Sequence[int], target: int) -> int:
    """First index with value >= target, or len(values) if none."""
    lo, hi = 0, len(values)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if values[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    return lo

if __name__ == "__main__":
    print(lower_bound([1, 3, 3, 7], 3))

The partition invariant and decreasing interval establish correctness and termination under sortedness. Comparing with bisect_left catches off-by-one mistakes across many examples, including randomly generated duplicates. It still covers finitely many inputs and may share a mistaken test setup, so it does not replace the argument. This implementation uses O(1)O(1) auxiliary storage and O(log⁡(n+1))O(\log(n+1)) indexed comparisons. Pre-sorting once costs additional time and may change the original ordering; it is not a free prerequisite.

Back to C02.05

I. Retention and readiness#

Retrieve prefix-sum state from C01, then explain a base case without saying merely “to stop recursion.” Advance when a new search variant can be specified with an invariant before code. Revisit functions and returns whenever debugging is reduced to adding print statements without a prediction.