Due: before class on Monday 09 November

  1. Select all TRUE statements in a language that has lexical scope and closures:

    1. When a function is called, the parent pointer of the newly created environment frame is set to the environment frame of the caller (the call site).

    2. Environment frames (especially in functions that return a lambda) must be allocated on the stack, not the heap.

    3. In a standard recursive Scheme function (e.g., (define (factorial n) ...)), each recursive call creates a new frame whose parent pointer points to the frame of the immediately preceding recursive call.

    4. Two closures created by two separate calls to the same outer factory function point to the same compiled code/AST node, but point to different environment frames.

    5. The return value of a function is always stored in the frame of the called function itself, not in the parent frame.

  2. Consider the following Scheme code:

    (define make-stepper
      (lambda (start step)
        (define current start)
        (lambda (msg . args)
          (cond
            ((eq? msg 'step!)
             (set! current (+ current step))
             current)
            ((eq? msg 'fork)
             (make-stepper current (car args)))
            ((eq? msg 'get)
             current)
            (else
             (error "Unknown msg"))))))
    
    (define s1 (make-stepper 10 2))
    (s1 'step!)
    (define s2 (s1 'fork 5))
    (s1 'step!)
    (s2 'step!)
    1. What are the three values produced by calling (s1 'step!) both (both times) and (s2 'step!)

    2. Draw the complete frames and closures diagram from running this program. Be sure to follow the “rules” on when to make a new closure vs. a new frame, and how to correctly set the parent frame.

  3. Write a higher-order Python function make_rate_limiter(max_calls) that returns a function which transforms any other one-argument function, into one that refuses to work after the given number of max_calls.

    That sentence is hard to read! Just check out this example on how make_rate_limiter should work:

    # Creates a limiter for up to 2 calls
    limit_twice = make_rate_limiter(2)
    
    # Wrap an arbitrary single-argument function
    greet = limit_twice(lambda name: f"Hello, {name}!")
    
    print(greet("Alice"))  # Prints: "Hello, Alice!"
    print(greet("Bob"))  # Prints: "Hello, Bob!"
    print(greet("Charlie"))  # Prints: "Rate limit exceeded"
    print(greet("Diana"))  # Prints: "Rate limit exceeded"
    
    # Calling limit_twice again resets the limit
    print(limit_twice(abs)(-5)) # Prints 5
    
    # Each wrapped function encapsulates its own independent state
    three_squares = make_rate_limiter(3)(lambda x: x * x)
    
    print(three_squares(3))  # Prints: 9
    print(three_squares(4))  # Prints: 16
    print(three_squares(5))  # Prints: 25
    print(three_squares(6))  # Prints: "Rate limit exceeded"