Split Loop
Intent
Section titled “Intent”A single loop is often pressed into doing two different things at once — summing one thing while finding the maximum of another, or collecting names while counting matches. Split it into two loops, each iterating the same collection but doing exactly one job. Each loop now reads as a single idea, and you are free to refactor or move each one independently.
The smell
Section titled “The smell”You read a loop and find yourself mentally untangling two threads: “this part builds the total… and this part, also in here, tracks the youngest person.” Two accumulators that have nothing to do with each other share a loop body only because they happen to walk the same list. That coupling makes the loop hard to name, hard to extract, and hard to change — touch the total logic and you risk the youngest-person logic.
Before → After
Section titled “Before → After”A loop over employees that computes total salary and, unrelatedly, finds the youngest age. Two jobs, one loop. We split them.
// Before — one loop, two unrelated jobsfunction report(people: Person[]): { total: number; youngest: number } { let total = 0; let youngest = people[0]?.age ?? Infinity; for (const p of people) { total += p.salary; if (p.age < youngest) youngest = p.age; } return { total, youngest };}
// After — each loop has a single purposefunction report(people: Person[]): { total: number; youngest: number } { return { total: totalSalary(people), youngest: youngestAge(people) };}
function totalSalary(people: Person[]): number { let total = 0; for (const p of people) total += p.salary; return total;}
function youngestAge(people: Person[]): number { let youngest = people[0]?.age ?? Infinity; for (const p of people) { if (p.age < youngest) youngest = p.age; } return youngest;}# Before — one loop, two unrelated jobsdef report(people): total = 0 youngest = people[0].age if people else float("inf") for p in people: total += p.salary if p.age < youngest: youngest = p.age return {"total": total, "youngest": youngest}
# After — each loop has a single purposedef report(people): return {"total": total_salary(people), "youngest": youngest_age(people)}
def total_salary(people): total = 0 for p in people: total += p.salary return total
def youngest_age(people): youngest = people[0].age if people else float("inf") for p in people: if p.age < youngest: youngest = p.age return youngest// Before — one loop, two unrelated jobsfunc report(people []Person) (total, youngest int) { youngest = math.MaxInt for _, p := range people { total += p.Salary if p.Age < youngest { youngest = p.Age } } return total, youngest}
// After — each loop has a single purposefunc report(people []Person) (int, int) { return totalSalary(people), youngestAge(people)}
func totalSalary(people []Person) int { total := 0 for _, p := range people { total += p.Salary } return total}
func youngestAge(people []Person) int { youngest := math.MaxInt for _, p := range people { if p.Age < youngest { youngest = p.Age } } return youngest}// Before — one loop, two unrelated jobsfn report(people: &[Person]) -> (i64, i64) { let mut total = 0; let mut youngest = i64::MAX; for p in people { total += p.salary; if p.age < youngest { youngest = p.age; } } (total, youngest)}
// After — each loop has a single purposefn report(people: &[Person]) -> (i64, i64) { (total_salary(people), youngest_age(people))}
fn total_salary(people: &[Person]) -> i64 { people.iter().map(|p| p.salary).sum()}
fn youngest_age(people: &[Person]) -> i64 { people.iter().map(|p| p.age).min().unwrap_or(i64::MAX)}Mechanics
Section titled “Mechanics”- Copy the entire loop so you have two identical loops over the same collection.
- In the first loop, delete the statements that belong to the second job. In the second loop, delete the statements that belong to the first. Each loop is now single-purpose.
- Run your tests. Behaviour must be unchanged.
- Apply Extract Function to each loop so it becomes a well-named query like
totalSalaryoryoungestAge. - Run your tests again — the caller now reads as two named results instead of one tangled loop.
When to use / trade-offs
Section titled “When to use / trade-offs”Split a loop whenever its body carries two responsibilities that you would describe with the word “and”. The payoff is clarity and reusability: each loop is easy to name, easy to extract, and easy to change in isolation — and Split Loop is usually the first step toward replacing each piece with a clear pipeline operation.
The obvious objection is performance: you now iterate twice. In nearly all real code that cost is negligible compared to the gain in readability, and a single iteration is a premature optimisation. If profiling later proves a hot loop matters, you can merge them back — but optimise from clean, well-named code, not from a tangle.