Skip to content
Part IA Michaelmas Term

Computing Length

Naive recursive length: nlength

let rec nlength = function
  | [] -> 0
  | x :: xs -> 1 + nlength xs
(* val nlength : 'a list -> int = <fun> *)

This is polymorphic and applies to all lists regardless of element type. A trace:

nlength [a; b; c] => 1 + nlength [b; c]
                  => 1 + (1 + nlength [c])
                  => 1 + (1 + (1 + nlength []))
                  => 1 + (1 + (1 + 0))
                  => 3

Like nsum, this function is not tail-recursive. The additions nest — 1 + (1 + (1 + 0)) — requiring O(n) space on the call stack.

Iterative length with accumulator: addlen

let rec addlen n = function
  | [] -> n
  | x :: xs -> addlen (n + 1) xs
(* val addlen : int -> 'a list -> int = <fun> *)

The function keyword introduces an extra (unnamed) argument that is pattern-matched. The integer accumulator n tracks the count so far:

addlen 0 [a; b; c] => addlen 1 [b; c]
                    => addlen 2 [c]
                    => addlen 3 []
                    => 3

The recursive calls do not nest — this is iterative and uses O(1) space.

Wrapper function

The efficient length function wraps addlen with an initial accumulator of zero:

let length xs = addlen 0 xs
(* val length : 'a list -> int = <fun> *)

Comparison

FunctionRecursionTimeSpace
nlengthNested (not tail-recursive)O(n)O(n)
addlen/lengthIterative (tail-recursive)O(n)O(1)

Both functions take O(n) time because you must examine each element to count it — there is no way around that. The accumulator improves space complexity only.

Why nlength is “naive”

The function nlength has the same flaw as nsum: it builds up deferred work (the 1 + expressions) that cannot be resolved until the base case is reached. For a list of length 10000, the call stack holds 10000 pending additions — a stack overflow waiting to happen. The iterative version addlen resolves the addition immediately (n + 1) and passes the result forward.