Higher-Order Functions

Practice SICP-style higher-order functions in Python: treat functions as data, reason about closures and composition, and build new behavior by returning and combining functions.

0. Functions as Data (SICP Ex 1.34 in Python)

**Predict the output** before running this code.

This is a Python version of SICP Exercise 1.34.

```python
def f(g):
    return g(2)

def square(x):
    return x * x

h = lambda z: z * (z + 1)

print(f(square))
print(f(h))
print(f(f))
```

1. What are the first two lines that get printed?
2. What happens when we try to evaluate `print(f(f))`?

Choose the best description:

A. Prints `4`, then `6`, then `4` again.

B. Prints `4`, then `6`, then `6` again.

C. Prints `4`, then `6`, then causes a **TypeError** before printing a third line.

D. Causes a **TypeError** on the very first call `f(square)`.

Write down your prediction in a comment, **then** run the code to check.

  

1. Tracing Composition and Closures

This exercise connects closures (functions that remember values) with function composition.

Consider this code, which is very close to the slide on composition:

```python
def compose(f, g):
    def h(x):
        return f(g(x))
    return h

def make_adder(n):
    def adder(x):
        return x + n
    return adder

square = lambda x: x * x

add1 = make_adder(1)
f = compose(square, add1)
result = f(5)
print(result)
```

1. **Predict** (before running): what will this program print?
2. **Trace the calls** that happen when evaluating `f(5)`:
   - Which functions are called, and in what order?
   - For each call, what are the argument values (`x`, `n`, etc.)?
   - Which frame does each inner function (like `adder` and `h`) *close over*?

Use comments in the starter code to write your trace: show the sequence of calls and how the value `36` is computed step by step.

  

2. Repeated Application via Composition (SICP Ex 1.43)

In SICP Exercise 1.43, we define the *n-th repeated application* of a function.

If `f` is a one-argument function and `n` is a positive integer, the n-th repeated application of `f` is the function that applies `f` to its argument **n times**.

Examples:
- If `f(x) = x + 1`, then the n-th repeated application is `x ↦ x + n`.
- If `f(x) = x * x` (square), then the n-th repeated application raises its input to the `2^n` power.

You already saw composition on the slides:

```python
def compose(f, g):
    def h(x):
        return f(g(x))
    return h
```

### Task

1. **Predict** before implementing:
   - What should `repeated(lambda x: x + 1, 5)(3)` return?
   - What should `repeated(lambda x: x * x, 2)(5)` return?
   - What about `repeated(lambda x: x * x, 3)(2)`?
2. **Implement** `repeated(f, n)` so that it returns a new function that applies `f` a total of `n` times.
   - You may use recursion **or** a `while`/`for` loop.
   - Try to use `compose` from above in your solution, as suggested in SICP.

This exercise is based on **SICP Exercise 1.43** and extends the composition idea from the lecture.