Beyond Enumerable: For Want of Better Windows | baweaver
baweaver
baweaver
Software Engineering
Simplicity is hard work. But there's a huge payoff. The person who has a genuinely simpler system is going to be able to affect the greatest change with the least work.<br>— Rich Hickey
ruby<br>May 31, 2026
Beyond Enumerable: For Want of Better Windows
Enumerable is probably one of, if not the most, powerful features of Ruby. It condenses several useful iteration patterns into more common language and allows us to focus on the problem at hand directly rather than by its component pieces like we might in a more imperative language.
The problem is that while it is indeed powerful, there’s only so much it can do, so where do we find ourselves with problems that lie beyond Enumerable? This series explores some of those shapes, and how the lessons we learned from Enumerable are still very much applicable once we step beyond it.
Our first step is into windows.
The bar Enumerable set
Enumerable is the standard by which many other Ruby libraries are judged for their fluency, legibility, and composability. It also represents a stark departure from what we might have found in languages like Java at a similar time before streams became popular.
Consider how you might sum every number over four that happens to be even and double each one in another language. You’d write a loop, an accumulator, and you’d have three distinct ideas inside the body of that loop doing the work of filtering, transforming, and summation:
sum = 0<br>for item in 1..100<br>sum += item * 2 if item > 4 && item.even?<br>end<br>sum
Certainly it works, but every one of those concerns is in the same place. Filtering, doubling, totaling, all of them are jammed into one body and changing any of them feels like you have to understand multiple components at once to do so. We’re dealing with primitives rather than problems in a way that does not quite feel expressive.
Enumerable lets you say the same thing as a sequence of named intentions:
(1..100).select { |v| v.even? && v > 4 }.map { |v| v * 2 }.sum
Each method call is one distinct idea. Keep the even numbers greater than four, and double them before adding them all up to get a final result. The loop, of course, is still there but you’re not thinking of the loop any more. You’re thinking of a series of steps describing the result rather than the pieces. You can pull any part our of that chain, rename it, test it independently, or swap it out. It’s not just a small convenience, it’s a different way of thinking about collections.
That’s the gift of Enumerable, if gives a name to common ways of moving through data:
map is a way to transform every element into another
select allows us to keep elements which match a condition
reduce and sum allow us to combine them all into a final result
You even have methods like group_by, tally, chunk_while, and partition among a series of others where each one is a name for a traversal you’d otherwise have to hand-roll with a series of loops and temporary variables. Once the name and the abstraction exist, the loop disappears and with it a whole category of bugs that came with it for what amount to fairly common implementations.
This is great, until you manage to find one of the problems that Enumerable doesn’t have a name for. The temptation might be to go back to manual loops, index variables, and accumulating by hand. Maybe we end up crawling with two-pointers or sliding windows along an array and juggle all the bookkeeping ourselves. Sometimes that’s the right call, certainly, but it just does not feel like Ruby now does it?
When we find enough of these common shapes we start noticing patterns, and patterns invite names and abstractions. Ruby taught us that naming and abstracting traversals allows us to say what we mean, clearly, and focus on problems. This doesn’t stop being true just because there’s no current name for it, if anything it’s an invitation to us to find those names and write abstractions which feel like Ruby.
Windows are one such shape, and that’s what this article intends to explore.
A problem you’ve actually had
Say you’re staring at a list of response times from a service, and you want a moving average to smooth out the noise. Three samples at a time, slide along, average each group.
You reach for each_cons, because that’s exactly what it’s for:
latencies.each_cons(3).map { |window| window.sum.fdiv(3) }
This is a Ruby-like answer that reads cleanly, says what it means, and lets you go on to solve other problems. each_cons presents us with a sliding window of a fixed size and for many problems that’s all we’ll need. Adjacent comparisons, n-grams, neighbors, all of it can be handled by each_cons
The problem you start to notice, though, is that every window that each_cons presents is a plain array. You end up adding all those numbers every time, and the window doesn’t remember anything about its current context. It’s just a slice.
For three...