← Back to all posts

The Recursion Template

AoC:25:07:2Gorecursionpatterns

The Mistake

Checking edge cases at call sites instead of in the base case, leading to tangled, confusing code.

My Gloomy Reality

I wrote this mess:

if isSplit(grid, row, col) {
    // here we need to recurse
    // try do left first
    // if there's a left
    if col-1 >= 0 {
        total += walk(grid, row+1, col-1)
        // do we return here?????
    } else {
        // this is dodgy af, I dont understand
        return total
    }

    // then do right, if theres a Right
    if col+1 < len(grid[0]) {
        total += walk(grid, row+1, col+1)
        // do we return here?????
    } else {
        // this is dodgy af, I dont understand
        return total
    }
}

Look at those comments: "do we return here?????" and "this is dodgy af, I dont understand".

I was confused because I was checking bounds before each recursive call. This scattered edge case logic throughout the function and made me unsure what to return when bounds failed.

The technique was fighting me, which meant I couldn't focus on the actual puzzle.

The Insight

Boundary checks go in the base case, not at call sites.

// ❌ BAD: checking before recursing
if col-1 >= 0 {
    total += walk(col-1)
} else {
    return total  // "dodgy af, I dont understand"
}

// ✅ GOOD: let invalid states reach the function
func walk(col int) int {
    if col < 0 { return 1 }  // forced to decide: what IS the answer here?
    return walk(col-1) + walk(col+1)
}

Why? Checking at call sites lets you avoid deciding what the answer is for invalid inputs. The base case forces you to answer: "a beam that goes off the edge contributes ___ to the total."

Once you answer that question, the recursion becomes clean.

The Fix

Follow this template:

func walk(state) Result {
    // 1. Base case(s): when to stop
    if done(state) { return baseValue }
    
    // 2. Recurse: combine results from children
    return combine(walk(next1), walk(next2), ...)
}

Three questions to answer:

  1. What's the state? (position, node, index...)
  2. When do I stop? (out of bounds, leaf node, target found...)
  3. How do I combine children? (sum, max, append, AND/OR...)

Day 7 beams:

func walk(grid []string, row, col int) int {
    // Base case: reached bottom or out of bounds
    if row >= len(grid) || col < 0 || col >= len(grid[0]) {
        return 1
    }
    
    // Recurse based on cell type
    if grid[row][col] == '^' {
        return walk(grid, row+1, col-1) + walk(grid, row+1, col+1)
    }
    return walk(grid, row+1, col)
}

No "dodgy af" confusion. Base case handles all edge conditions. Recursive case is clean.

Recursion vs Iteration

Both can solve the same problems. Pick whichever you can write correctly faster.

Signals for recursion:

Signals for iteration:

For AoC: Start with recursion. It usually matches how the problem is described. Add memoization if slow. Only convert to iteration if you hit stack limits (rare in AoC).

Lesson Learned

Simplify the technique so you can focus on the puzzle.

When the recursion template is automatic—base case first, recurse second—you stop thinking about how to recurse and start thinking about what the problem actually needs.

The "dodgy af" comments were me fighting the technique. The clean version freed my brain to think about beams and splitters instead of if statements and return values.


Original puzzle: Advent of Code 2025 Day 7