26  Advanced Functions: Recursive, Closure, and Nested

NoteWhat this chapter covers

Three advanced patterns that build on the function basics from the previous chapter. Recursive functions call themselves to break a problem into a smaller version of itself. Closures are functions that remember the environment in which they were created, the foundation of function factories and stateful counters. Nested functions are helper functions defined inside another function, used to organise larger pieces of logic without polluting the workspace. By the end you will be able to write each pattern, recognise when each is the right tool, and avoid the traps each carries.

26.1 Recursive functions

A recursive function calls itself. Used well, recursion turns a complex problem into a tiny one, solve the easy case, and reduce every other case to a smaller version of the same problem.

Every recursive function needs two pieces:

  1. A base case, a stopping condition that returns directly without recursing.
  2. A recursive case, work that reduces the problem and calls the function again on the smaller piece.

The classic textbook example, factorial:

factorial_r(5) calls factorial_r(4), which calls factorial_r(3), all the way down to factorial_r(1), which returns 1 and lets the chain unwind, multiplying as it goes.

A second example, Fibonacci numbers:

WarningRecursion is elegant but not always efficient

The Fibonacci version above recomputes the same values an exponential number of times. fib(10) is fine; fib(35) already crawls. Two fixes are common:

  • Iteration, rewrite as a for loop with two running variables.
  • Memoisation, cache results so each n is computed once.

R also caps the recursion depth (default ~5000). For deep recursion, switch to iteration.

The general rule: use recursion when the problem itself is recursive, tree traversal, parsing nested structures, divide-and-conquer algorithms, not as a default style.

26.2 A useful recursive example, sum of nested list

A list can contain lists, which can contain more lists. Computing the total of all the numbers buried in such a structure is a natural recursive task.

Notice how sapply(x, deep_sum) does the work, each element of the list is recursively summed by the same function. That is the recursive pattern in its purest form.

26.3 Closures

A closure is a function that “remembers” the environment in which it was defined. When you create a function inside another function, the inner function carries a reference to the outer function’s local variables, even after the outer function has returned.

double and triple are both anonymous function(x) x * factor bodies, but each carries its own captured value of factor. That captured environment is the closure.

This is called a function factory: a function whose job is to build other functions tailored to a parameter.

26.4 Closures with state

Because the captured environment is mutable (with the right operator), a closure can keep state across calls.

Two new things to spot:

  • count lives in the environment created when make_counter() ran. Each call to the returned function reads and updates that environment.
  • The double-arrow <<- is the super-assignment operator. A plain <- inside the inner function would create a fresh local count and leave the captured one untouched. <<- walks up the parent environments until it finds an existing binding to update.

Two independent counters live independent lives:

TipWhen closures earn their keep
  • Configuration captured once, called many times, formatters with a fixed locale, validators with a fixed schema.
  • Stateful generators, counters, samplers, ID issuers.
  • Partial application, fixing some arguments now and leaving the rest for later.

Use them sparingly. Most R analysis code doesn’t need them, but when it does, nothing else is as clean.

26.5 Nested functions

A nested function is a helper defined inside a larger function purely to organise the logic. It exists only while the outer function runs; nothing in the workspace can see it.

Three reasons to nest:

  1. The helper is specific to one function and useless on its own.
  2. Nesting documents the relationship, a reader can see immediately that grade() is part of report_card()’s machinery.
  3. The helper doesn’t clutter the workspace.

When a helper starts being useful in more than one place, promote it to a top-level function.

26.6 Recursion + closure together

The two ideas combine naturally. Here is a memoised Fibonacci, recursion for elegance, closure for the cache.

The closure carries cache; the recursion drives the calculation; together they compute fib(20) in linear time. This is a microcosm of how the three ideas combine in real code, each pattern earning its place by solving a specific problem.

26.7 Worked example, a configurable validator factory

A function factory that builds validators for student admission rules. Each call returns a validator with its own captured cutoffs, useful when different streams have different rules.

Three patterns from this chapter, all in one example:

  • The factory (make_validator) is itself a function that returns functions.
  • Each returned function is a closure carrying its own min_marks, min_attendance, and allow_exemption.
  • The body of the returned function is vectorised, so it applies to a whole class at once, no loop needed.

26.8 Summary

Pattern What it does When to use
Recursive function calls itself, with a base case naturally recursive structures (trees, nested lists, divide-and-conquer)
Closure function captures and remembers an enclosing environment function factories, stateful generators, partial application
Nested helper function defined inside another function helper used only inside the outer function, kept off the workspace

Three rules that keep these tools honest:

  1. Every recursive function needs a base case. Forget it and the call stack overflows.
  2. Use <<- only when you mean to mutate a captured variable. Inside a normal function, plain <- is almost always what you want.
  3. Promote a nested helper to a top-level function as soon as a second caller wants it. Don’t copy-paste an internal helper into another function.

Together with the basics from Chapter 25, these three patterns cover almost every function-shaped problem you’ll meet. Functions are the building blocks; loops and apply functions are how you orchestrate them; conditionals decide what each function should do. With the four chapters of Part 4 behind you, you have the entire control-flow toolkit of R.