Skip to the code
cf.

Trampoline

MediumFunctions

fn is a recursive function written so that it never calls itself directly. Each call returns either the final value, or a thunk: a function with no parameters that performs the next step when it's called.

Write trampoline(fn). It returns a new function. Calling that function calls fn with the same this and arguments. Then, as long as the result is a function, it calls that function with no arguments and takes its result instead. The first result that isn't a function is returned, whatever its type.

The stack must not grow with the number of steps: a function that takes 100,000 steps, more nested calls than any browser's stack can hold, must run without a stack overflow.

const sumTo = trampoline(function step(n, total = 0) {
  return n === 0 ? total : () => step(n - 1, total + n);
});
sumTo(3);       // 6: step(3, 0), then step(2, 3), step(1, 5), and step(0, 6)
sumTo(100_000); // 5000050000, with no stack overflow

What the tests check

  1. returns fn's result when it isn't a function
    const fn = spy((x) => x * 2);
    expect(trampoline(fn)(21)).toBe(42);
    expect(fn).toHaveBeenCalledTimes(1);
    
  2. calls each thunk until it gets a value
    const thunks = [];
    const sumTo = trampoline(function step(n, total = 0) {
      if (n === 0) return total;
      const thunk = spy(() => step(n - 1, total + n));
      thunks.push(thunk);
      return thunk;
    });
    expect(sumTo(3)).toBe(6);
    expect(thunks).toHaveLength(3);
    expect(thunks.map((thunk) => thunk.callCount)).toEqual([1, 1, 1]);
    
  3. runs 100,000 steps without overflowing the stack
    const sumTo = trampoline(function step(n, total = 0) {
      return n === 0 ? total : () => step(n - 1, total + n);
    });
    expect(sumTo(100_000)).toBe(5000050000);
    
  4. works for functions that bounce between each other
    const isEven = (n) => (n === 0 ? true : () => isOdd(n - 1));
    const isOdd = (n) => (n === 0 ? false : () => isEven(n - 1));
    expect(trampoline(isEven)(100001)).toBe(false);
    expect(trampoline(isOdd)(100001)).toBe(true);
    
  5. returns the final value whatever its type
    const last = { id: 3 };
    const walk = trampoline(function step(node) {
      return node.next ? () => step(node.next) : node;
    });
    expect(walk({ id: 1, next: { id: 2, next: last } })).toBe(last);
    expect(trampoline(() => () => "done")()).toBe("done");
    expect(trampoline(() => () => 0)()).toBe(0);
    expect(trampoline(() => () => null)()).toBeNull();
    expect(trampoline(() => () => undefined)()).toBeUndefined();
    
  6. passes the caller's this and arguments to fn
    const fn = spy(function (a, b) {
      return this.base + a + b;
    });
    const obj = { base: 100, run: trampoline(fn) };
    expect(obj.run(1, 2)).toBe(103);
    expect(fn.calls[0].this).toBe(obj);
    expect(fn.calls[0].args).toEqual([1, 2]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).