Coding Practice / Module 9 · Closures / Problems 81–90

Module 9: Closures & Function Composition

A closure is a function that remembers the variables from the scope it was created in, even after that scope has finished running. This module builds ten small, genuinely useful utilities on top of that one idea — counters, private state, currying, debounce/throttle, memoization and pipelines — the exact toolkit that shows up constantly in frontend interviews.

Module 9 of 12 Problems 81–90 JS + Python ~60 Min

By the end of this module, you'll be able to

  • Explain what a closure actually captures, and why it keeps working after the outer function has returned
  • Tell debounce and throttle apart, and know which one fits a search box vs. a scroll handler
  • Compose small functions into a pipeline instead of nesting calls inside calls

1. Problems 81–90

Same format as the previous modules: expand a problem to see the approach and both commented solutions.

Python closures need one extra keyword

JavaScript lets an inner function reassign an outer variable with no special syntax. Python requires nonlocal before doing that — without it, count += 1 inside a nested function would silently create a brand-new local variable instead of modifying the outer one. Watch for it in the examples below.

P81

Counter Using Closure

Each call to the returned function increments a hidden count.

Approach: the outer function declares a variable and returns an inner function that reads and updates it. The inner function keeps a private reference to that variable — a closure — even after the outer function has already finished running.

JavaScript
create-counter.js
// The returned function "closes over" `count` -- it keeps a private reference
// to that variable even after createCounter() has finished running.
function createCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Python
create_counter.py
def create_counter():
    """A nested function reads and modifies the enclosing variable via `nonlocal`."""
    count = 0
    def increment():
        nonlocal count  # without this, count += 1 below would create a new local variable
        count += 1
        return count
    return increment

counter = create_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3
P82

Bank Account Using Closure (Private State)

The balance can only change through deposit/withdraw, never directly.

Approach: the balance variable lives only inside the closure's scope. The only way to read or change it is through the functions returned alongside it — which is what "private" means here, without a real access-modifier keyword.

JavaScript
create-bank-account.js
// balance is never exposed directly -- only these two functions can touch it,
// which is what "private" means without a real access-modifier keyword.
function createBankAccount(initialBalance) {
  let balance = initialBalance;
  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() { return balance; },
  };
}

const account = createBankAccount(100);
account.deposit(50);
account.withdraw(30);
console.log(account.getBalance()); // 120
Python
create_bank_account.py
def create_bank_account(initial_balance: float):
    """Same idea: `balance` only exists inside this closure's scope."""
    balance = initial_balance

    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance

    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            raise ValueError("Insufficient funds")
        balance -= amount
        return balance

    def get_balance():
        return balance

    return {"deposit": deposit, "withdraw": withdraw, "get_balance": get_balance}

account = create_bank_account(100)
account["deposit"](50)
account["withdraw"](30)
print(account["get_balance"]())  # 120
P83

Function Currying

Turn add(a,b,c) into add(a)(b)(c) — one argument per call.

Approach: each call collects one more argument into a closure. Once enough arguments have accumulated, the original function finally runs with all of them; until then, every call just returns another function waiting for the rest.

JavaScript
curry.js
// Curry turns add(a, b, c) into add(a)(b)(c) -- each call captures one more
// argument in a closure until all three have been supplied.
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) return fn(...args); // enough args -- call it
    return (...more) => curried(...args, ...more); // not enough yet -- keep collecting
  };
}

function add3(a, b, c) { return a + b + c; }
const curriedAdd = curry(add3);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
Python
curry.py
def curry(fn, arity):
    """Python has no fn.length, so the expected argument count is passed in explicitly."""
    def curried(*args):
        if len(args) >= arity:
            return fn(*args)  # enough args -- call it
        return lambda *more: curried(*args, *more)  # not enough yet -- keep collecting
    return curried

def add3(a, b, c):
    return a + b + c

curried_add = curry(add3, 3)
print(curried_add(1)(2)(3))  # 6
print(curried_add(1, 2)(3))  # 6
P84

once() — Run a Function Only Once

Later calls return the cached result instead of running again.

Approach: a flag and a result variable, both closed over — the first call runs the real function and saves its result; every call after that just returns what was saved, skipping the real function entirely.

JavaScript
once.js
// A flag inside the closure remembers whether the wrapped function has already
// run; every call after the first just returns the cached result.
function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      result = fn(...args);
      called = true;
    }
    return result;
  };
}

const initialize = once(() => { console.log("Initializing..."); return "ready"; });
console.log(initialize()); // logs "Initializing...", returns "ready"
console.log(initialize()); // logs nothing, returns "ready" again
Python
once.py
def once(fn):
    """Same flag-and-cache trick, using a mutable dict since Python closures can't
    reassign an outer variable without `nonlocal` (a dict's contents don't need it)."""
    state = {"called": False, "result": None}
    def wrapper(*args, **kwargs):
        if not state["called"]:
            state["result"] = fn(*args, **kwargs)
            state["called"] = True
        return state["result"]
    return wrapper

def _initialize():
    print("Initializing...")
    return "ready"

initialize = once(_initialize)
print(initialize())  # logs "Initializing...", returns "ready"
print(initialize())  # logs nothing, returns "ready" again
P85

memoize() — Cache Results by Argument

An expensive call only ever runs once per distinct input.

Approach: like once(), but keyed by argument instead of a single flag — a cache (Map/dict) remembers the result for each distinct set of arguments already seen, and skips recomputation on a repeat.

JavaScript
memoize.js
// Store each call's result keyed by its (stringified) arguments -- an expensive
// call only ever runs once per distinct input.
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key); // seen these args before -- skip the work
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

const slowSquare = (n) => { for (let i = 0; i < 1e6; i++); return n * n; }; // pretend this is slow
const fastSquare = memoize(slowSquare);
console.log(fastSquare(5)); // computed
console.log(fastSquare(5)); // returned instantly from cache
Python
memoize.py
def memoize(fn):
    cache = {}
    def wrapper(*args):
        if args in cache:  # a tuple of args works directly as a dict key in Python
            return cache[args]
        result = fn(*args)
        cache[args] = result
        return result
    return wrapper

def _slow_square(n):
    for _ in range(1_000_000):  # pretend this is slow
        pass
    return n * n

fast_square = memoize(_slow_square)
print(fast_square(5))  # computed
print(fast_square(5))  # returned instantly from cache
# Idiomatic Python: functools.lru_cache does this automatically as a decorator.
P86

debounce() — Wait for a Pause Before Running

Only the last call in a rapid burst actually fires.

Approach: every call cancels whatever timer the previous call scheduled, then starts a fresh one — the wrapped function only actually runs once calls stop arriving for the full delay. Perfect for a search box: don't hit the API on every keystroke, only once typing pauses.

JavaScript
debounce.js
// Every call cancels the previous pending timer and starts a new one -- the
// wrapped function only actually runs once calls stop arriving for `delay` ms.
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer); // cancel whatever was scheduled before
    timer = setTimeout(() => fn(...args), delay);
  };
}

const search = debounce((value) => console.log("API:", value), 500);
search("h");
search("he");
search("hel"); // only this final call actually fires, 500ms after it's made
Python
debounce.py
import threading

def debounce(fn, delay_seconds):
    """Python has no setTimeout, so a Timer thread stands in for the browser's timer."""
    state = {"timer": None}
    def wrapper(*args, **kwargs):
        if state["timer"] is not None:
            state["timer"].cancel()  # cancel whatever was scheduled before
        state["timer"] = threading.Timer(delay_seconds, lambda: fn(*args, **kwargs))
        state["timer"].start()
    return wrapper

search = debounce(lambda value: print("API:", value), 0.5)
search("h")
search("he")
search("hel")  # only this final call actually fires, 0.5s after it's made
P87

throttle() — Limit How Often a Function Can Run

Guarantees at most one run per interval, unlike debounce.

Approach: remember the timestamp of the last allowed call. A new call only goes through once enough time has passed since then — unlike debounce, throttle keeps firing steadily through a continuous stream of calls instead of waiting for them to stop. Better fit for a scroll handler.

JavaScript
throttle.js
// Unlike debounce, throttle guarantees the function runs at most once per
// interval, even if calls keep arriving the whole time.
function throttle(fn, interval) {
  let lastCall = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastCall >= interval) {
      lastCall = now;
      fn(...args); // enough time has passed -- allow this call through
    }
  };
}

const onScroll = throttle(() => console.log("scroll handled"), 1000);
// Rapid-fire calls to onScroll() over 3 seconds would log roughly 3 times, not dozens.
Python
throttle.py
import time

def throttle(fn, interval_seconds):
    state = {"last_call": 0}
    def wrapper(*args, **kwargs):
        now = time.monotonic()
        if now - state["last_call"] >= interval_seconds:
            state["last_call"] = now
            fn(*args, **kwargs)  # enough time has passed -- allow this call through
    return wrapper

on_scroll = throttle(lambda: print("scroll handled"), 1.0)
# Rapid-fire calls to on_scroll() over 3 seconds would log roughly 3 times, not dozens.
P88

retry() — Retry a Failing Function N Times

Call it again on failure, up to a limit, before giving up.

Approach: loop up to maxAttempts times. If the call succeeds, return immediately; if it throws and attempts remain, log it and loop again; if the final attempt also fails, let the error escape for real.

JavaScript
retry.js
// Call the function; if it throws, try again up to `maxAttempts` times before
// finally letting the error escape.
function retry(fn, maxAttempts) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return fn(); // success -- stop retrying
    } catch (err) {
      if (attempt === maxAttempts) throw err; // out of attempts -- give up
      console.log(`Attempt ${attempt} failed, retrying...`);
    }
  }
}

let tries = 0;
const flaky = () => { tries++; if (tries < 3) throw new Error("fail"); return "success"; };
console.log(retry(flaky, 5)); // "success" (after two logged retries)
Python
retry.py
def retry(fn, max_attempts: int):
    for attempt in range(1, max_attempts + 1):
        try:
            return fn()  # success -- stop retrying
        except Exception as err:
            if attempt == max_attempts:
                raise  # out of attempts -- give up
            print(f"Attempt {attempt} failed, retrying...")

tries = 0
def flaky():
    global tries
    tries += 1
    if tries < 3:
        raise ValueError("fail")
    return "success"

print(retry(flaky, 5))  # "success" (after two printed retries)
P89

pipe() and compose() — Chain Functions Together

pipe runs left to right; compose runs right to left.

Approach: both take a list of single-argument functions and return one new function that threads a value through all of them, in the given order (pipe) or the reverse order (compose) — a direct application of Module 6's reduce.

JavaScript
pipe-compose.js
// pipe() runs functions left to right; compose() runs them right to left.
// Both thread one value through a list of single-argument functions.
function pipe(...fns) {
  return (initial) => fns.reduce((value, fn) => fn(value), initial);
}

function compose(...fns) {
  return (initial) => fns.reduceRight((value, fn) => fn(value), initial);
}

const double = (n) => n * 2;
const addOne = (n) => n + 1;

console.log(pipe(double, addOne)(5));    // (5*2)+1 = 11
console.log(compose(double, addOne)(5)); // (5+1)*2 = 12
Python
pipe_compose.py
from functools import reduce

def pipe(*fns):
    """Runs functions left to right, threading one value through all of them."""
    return lambda initial: reduce(lambda value, fn: fn(value), fns, initial)

def compose(*fns):
    """Runs functions right to left."""
    return lambda initial: reduce(lambda value, fn: fn(value), reversed(fns), initial)

double = lambda n: n * 2
add_one = lambda n: n + 1

print(pipe(double, add_one)(5))     # (5*2)+1 = 11
print(compose(double, add_one)(5))  # (5+1)*2 = 12
P90

Partial Application

Pre-fill some arguments, get back a smaller function.

Approach: similar to currying, but simpler — fix some arguments up front in a closure, and return a new function that only needs the remaining ones to actually call the original.

JavaScript
partial.js
// A partially applied function pre-fills some arguments and returns a new,
// smaller function waiting for the rest.
function partial(fn, ...presetArgs) {
  return (...remainingArgs) => fn(...presetArgs, ...remainingArgs);
}

function greet(greeting, name) { return `${greeting}, ${name}!`; }
const sayHello = partial(greet, "Hello");
console.log(sayHello("Amit")); // "Hello, Amit!"
Python
partial.py
def partial(fn, *preset_args):
    """Same idea; Python's standard library even ships this as functools.partial."""
    return lambda *remaining_args: fn(*preset_args, *remaining_args)

def greet(greeting, name):
    return f"{greeting}, {name}!"

say_hello = partial(greet, "Hello")
print(say_hello("Amit"))  # "Hello, Amit!"
# Idiomatic Python: from functools import partial

2. Key Takeaways

  • A closure is just a function plus the variables it can still reach from where it was defined — every problem here (counter, private state, cache, timer) is that same mechanism used for a different job.
  • debounce waits for a pause and fires once; throttle fires steadily no more than once per interval. Mixing them up is a very common interview mistake.
  • Currying, partial application, memoization, pipe and compose are all thin wrappers that take a function in and hand a new function back out — a pattern worth recognizing on sight, since interview "implement X" questions usually turn out to be one of these in disguise.