← Back to the lab

Problem 1: Recursion over nested lists

For population.

Recursion over one list

A list is built from two constructors: [] for empty, h :: t for a head h and tail t. A function that walks a list almost always has one match clause per constructor, doing some work with h and recursing on t.

let rec length l =
  match l with
  | []     -> 0             (* base case *)
  | _ :: t -> 1 + length t  (* recursive case *)

let _ = length [10; 20; 30]  (* = 3 *)

length does its real work after the recursive call returns. 1 + length t still has an addition left to do once length t comes back. A function like this one doesn't need to be tail-recursive to work correctly, only to avoid a stack overflow on very large inputs.

One level of nesting, one more helper

A list of lists, a document as a list of sentences where each sentence is a list of words, has two levels of structure. Walking all of it means recursing at both levels: one helper that handles a single inner list, and a second helper, built the same way, that handles the list of those lists and combines each one's own result with the running total.

let rec count_words sentence =
  match sentence with
  | []      -> 0
  | _ :: ws -> 1 + count_words ws

let rec count_document paragraph =
  match paragraph with
  | []             -> 0
  | sentence :: ps -> count_words sentence + count_document ps

let _ = count_document [["a"; "cat"]; ["sat"]]  (* = 3 *)

count_words is length under a different name. It recurses down one sentence, adding 1 per word. count_document follows the same shape one level up. It recurses down the list of sentences, and for each one adds that sentence's own word count, found by calling count_words on it, to the running total from the rest of the document. Neither helper is tail-recursive. Both do their addition on the way back up the call stack, after their recursive call returns. That's the right shape here: two separate additions to perform, one per level of nesting, not one running total threaded all the way through.

Why two helpers?

count_words and count_document take different kinds of argument, one sentence versus a list of sentences, and recurse over different lists, so they can't be the same function. count_document's job at each step stays simple: find the count for one sentence by calling count_words, and add it to the count for the rest. One recursive helper per level of nesting, each calling into the level below it, is the standard shape for a function over a nested list.