Skip to content

A First Example

Recursion is a technique where a function calls itself.

Recursion

Recursion is a technique where a function calls itself.

Instead of solving a problem all at once, the function solves a smaller version of the same problem repeatedly.

A recursive function usually has two parts:

  • a base case
  • a recursive case

The base case stops the recursion.

The recursive case calls the function again with a smaller problem.

A First Example

const std = @import("std");

fn countdown(n: u32) void {
    if (n == 0) {
        std.debug.print("Done!\n", .{});
        return;
    }

    std.debug.print("{}\n", .{n});

    countdown(n - 1);
}

pub fn main() void {
    countdown(5);
}

Output:

5
4
3
2
1
Done!

The function calls itself repeatedly:

countdown(5)
countdown(4)
countdown(3)
countdown(2)
countdown(1)
countdown(0)

The recursion stops at:

if (n == 0)

This is the base case.

Why the Base Case Matters

Without a base case, recursion never stops.

Bad example:

fn broken() void {
    broken();
}

This causes infinite recursion.

The program keeps creating function calls until the stack overflows.

Eventually the program crashes.

Every recursive function must move toward a stopping condition.

Recursive Factorial

A classic recursion example is factorial.

Mathematically:

5! = 5 × 4 × 3 × 2 × 1

Also:

5! = 5 × 4!

This naturally fits recursion.

Implementation:

fn factorial(n: u32) u32 {
    if (n == 0) {
        return 1;
    }

    return n * factorial(n - 1);
}

Calling:

const result = factorial(5);

Result:

120

Understanding Recursive Execution

Let us expand:

factorial(5)

This becomes:

5 * factorial(4)
5 * (4 * factorial(3))
5 * (4 * (3 * factorial(2)))
5 * (4 * (3 * (2 * factorial(1))))
5 * (4 * (3 * (2 * (1 * factorial(0)))))

Base case:

factorial(0) = 1

Then execution unwinds:

1 * 1 = 1
2 * 1 = 2
3 * 2 = 6
4 * 6 = 24
5 * 24 = 120

This “unwinding” process is very important in recursion.

Recursive Fibonacci

Another classic example is Fibonacci numbers.

Definition:

F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2)

Implementation:

fn fibonacci(n: u32) u32 {
    if (n == 0) {
        return 0;
    }

    if (n == 1) {
        return 1;
    }

    return fibonacci(n - 1) + fibonacci(n - 2);
}

Calling:

const result = fibonacci(10);

Result:

55

Recursive Tree Thinking

Recursion often creates a tree of calls.

For:

fibonacci(4)

the calls look like:

fibonacci(4)
├── fibonacci(3)
│   ├── fibonacci(2)
│   │   ├── fibonacci(1)
│   │   └── fibonacci(0)
│   └── fibonacci(1)
└── fibonacci(2)
    ├── fibonacci(1)
    └── fibonacci(0)

Notice repeated work:

fibonacci(2)

is computed multiple times.

This makes naive Fibonacci recursion slow.

Recursive Directory Traversal

Recursion is common in file systems.

Folders contain folders.

A recursive algorithm naturally matches this structure.

Conceptually:

fn visitDirectory(path: []const u8) void {
    // process files

    // recursively visit subdirectories
}

The function processes one directory, then recursively processes children.

Recursive Data Structures

Some data structures are recursive themselves.

Example:

const Node = struct {
    value: i32,
    next: ?*Node,
};

A linked list node points to another node.

Trees are also recursive.

Example:

const TreeNode = struct {
    value: i32,
    left: ?*TreeNode,
    right: ?*TreeNode,
};

Recursive functions are natural for recursive structures.

Recursive Tree Traversal

Example conceptual traversal:

fn visit(node: ?*TreeNode) void {
    if (node == null) {
        return;
    }

    visit(node.?.left);

    // process node

    visit(node.?.right);
}

This pattern appears everywhere in compilers, parsers, databases, and game engines.

Stack Frames

Every function call creates a stack frame.

A stack frame stores:

  • local variables
  • parameters
  • return information

Recursive calls create many stack frames.

Example:

factorial(5)
factorial(4)
factorial(3)
factorial(2)
factorial(1)
factorial(0)

Each call consumes stack memory.

Too many recursive calls can overflow the stack.

Stack Overflow

Dangerous recursion:

fn recurse() void {
    recurse();
}

Eventually:

stack overflow

This is why recursive algorithms must reduce the problem size.

Tail Recursion

Tail recursion happens when the recursive call is the final operation.

Example:

fn countdown(n: u32) void {
    if (n == 0) {
        return;
    }

    return countdown(n - 1);
}

Some compilers optimize this into loops.

This is called tail-call optimization.

Whether optimization occurs depends on compiler behavior and circumstances.

Recursion vs Loops

Many recursive algorithms can also be written with loops.

Recursive factorial:

fn factorial(n: u32) u32 {
    if (n == 0) {
        return 1;
    }

    return n * factorial(n - 1);
}

Iterative factorial:

fn factorialIterative(n: u32) u32 {
    var result: u32 = 1;
    var i: u32 = 1;

    while (i <= n) : (i += 1) {
        result *= i;
    }

    return result;
}

Both compute the same result.

Loops are often more memory-efficient.

Recursion is often more elegant for tree-like problems.

When Recursion Is Useful

Recursion works especially well for:

  • trees
  • graphs
  • parsers
  • file systems
  • divide-and-conquer algorithms
  • mathematical definitions

Examples:

  • quicksort
  • mergesort
  • expression parsing
  • JSON traversal
  • AST walking
  • game search algorithms

When Recursion Is Dangerous

Recursion can become problematic when:

  • recursion depth becomes very large
  • stack usage becomes excessive
  • repeated work makes algorithms slow
  • termination conditions are unclear

Naive Fibonacci is a classic inefficient recursion example.

Recursive Parsing

Compilers frequently use recursion.

Expression grammars naturally become recursive functions.

Conceptual parser:

fn parseExpression() void {
    parseTerm();
}

Then:

fn parseTerm() void {
    parseFactor();
}

This style is called recursive descent parsing.

It is extremely common in language tooling.

A Complete Example

const std = @import("std");

fn sum(values: []const i32) i32 {
    if (values.len == 0) {
        return 0;
    }

    return values[0] + sum(values[1..]);
}

pub fn main() void {
    const numbers = [_]i32{ 1, 2, 3, 4, 5 };

    const result = sum(&numbers);

    std.debug.print("{}\n", .{result});
}

Output:

15

Execution:

1 + sum([2,3,4,5])
2 + sum([3,4,5])
3 + sum([4,5])
4 + sum([5])
5 + sum([])
0

Then the results combine while unwinding.

Mental Model

Recursion means:

solve a problem using smaller versions of the same problem

A recursive function repeatedly reduces the problem until reaching a simple stopping case.

The essential ingredients are:

PartPurpose
Base casestops recursion
Recursive casereduces the problem
Progressmoves toward termination

Without all three, recursion fails.

Recursion is one of the most important ideas in computer science because many real-world structures are naturally recursive.