Replace Loop with Pipeline
Intent
Section titled “Intent”A loop that walks a collection, skips some elements, transforms others, and accumulates a result is doing several jobs braided together. A collection pipeline untangles them: each operation — filter, map, reduce — becomes a separate, named stage, and the data flows through them in order. The reader follows the data instead of tracking a mutable accumulator and an index. The behaviour is identical; the shape changes from “how to iterate” to “what each step does”.
The smell
Section titled “The smell”You see a for loop with a result variable declared just above it, a continue or guarding if inside, and a transformation buried in the middle. To understand it you must mentally execute it, holding the accumulator’s running state in your head. The loop conflates selecting which elements count, transforming them, and combining them — three distinct ideas with no visible seam.
Before → After
Section titled “Before → After”Collect the names of active premium customers from a region. Imperatively this is one loop with a guard, a field access, and an append; as a pipeline it is filter, then filter, then map.
// Beforefunction premiumNames(customers: Customer[], region: string): string[] { const names: string[] = []; for (const c of customers) { if (c.region !== region) continue; if (!c.isPremium) continue; names.push(c.name.toUpperCase()); } return names;}
// Afterfunction premiumNames(customers: Customer[], region: string): string[] { return customers .filter((c) => c.region === region) .filter((c) => c.isPremium) .map((c) => c.name.toUpperCase());}# Beforedef premium_names(customers, region): names = [] for c in customers: if c.region != region: continue if not c.is_premium: continue names.append(c.name.upper()) return names
# Afterdef premium_names(customers, region): in_region = (c for c in customers if c.region == region) premium = (c for c in in_region if c.is_premium) return [c.name.upper() for c in premium]// Beforefunc PremiumNames(customers []Customer, region string) []string { names := []string{} for _, c := range customers { if c.Region != region { continue } if !c.IsPremium { continue } names = append(names, strings.ToUpper(c.Name)) } return names}
// After — Go has no built-in pipeline, so keep one loop with named stepsfunc PremiumNames(customers []Customer, region string) []string { names := []string{} for _, c := range customers { inRegion := c.Region == region isPremium := c.IsPremium if inRegion && isPremium { names = append(names, strings.ToUpper(c.Name)) } } return names}// Beforefn premium_names(customers: &[Customer], region: &str) -> Vec<String> { let mut names = Vec::new(); for c in customers { if c.region != region { continue; } if !c.is_premium { continue; } names.push(c.name.to_uppercase()); } names}
// Afterfn premium_names(customers: &[Customer], region: &str) -> Vec<String> { customers .iter() .filter(|c| c.region == region) .filter(|c| c.is_premium) .map(|c| c.name.to_uppercase()) .collect()}Mechanics
Section titled “Mechanics”- Find the collection the loop walks and identify the result it builds.
- Translate each piece of loop logic into a stage. A
continue-style guard becomes afilter; a per-element transformation becomes amap; the accumulation of a single value becomes areduce(orsum,join, and friends). - Build the pipeline one stage at a time, leaving the loop in place beside it if that helps you compare.
- Replace the loop with the finished pipeline and return its result.
- Run your tests. The output must match the loop exactly, including for an empty input.
- In a language without built-in pipelines, such as Go, do not force one: keep a single loop but lift each condition into a clearly named boolean so the steps still read as distinct ideas.
When to use / trade-offs
Section titled “When to use / trade-offs”Reach for a pipeline when a loop mixes filtering, transforming, and accumulating — the stages make each intention explicit and let you read the data’s journey from top to bottom. It shines when the operations are pure and order-independent.
The trade-offs: a loop with early break, an index that matters, or side effects in the body can be awkward or misleading as a pipeline, and a very long chain can become its own kind of puzzle. Some pipelines also allocate intermediate collections. Use it where it clarifies, and remember Go’s note above — a named-step loop is the idiomatic equivalent there.