A big reason I love Ruby is how much work I can get done in just a few characters or lines of code, and ensuring that code is still easy to read for my peers. One area where this is most apparent is in dealing with arrays and hashes, also known as enumerables in the Ruby world.
Any object that includes Ruby’s powerful Enumerable module can be iterated over, traversed, manipulated, sliced and diced in various ways. This module’s flexibility leads to surprisingly terse code for complex tasks.
When Ruby 2.6 arrived in December 2018, it came with some new methods for Enumerable as well as other improvements to list and sequence handling. In this post, I’ll show you a few capabilities recently added to Ruby’s Enumerable as well as a few old favorites.
Endless ranges
Ranges are a great Ruby feature that allows you to quickly create iterable ranges from numbers or letters, such as 1..10
or ('A'..'Z')
. Before Ruby 2.6, the double-dot range syntax required a start and a finish. Now, there’s an intuitive syntax to create these endless ranges—simply omit a final character after the double dots: 1..
.
Why are endless ranges useful? One obvious use case is as a clean way to generate an ever-growing list of integers:
(1..).each do |i|
puts i
end
# 1
# 2
# 3
# ...
Another unique way to use endless ranges is to use them in concert with other methods chained onto enumerable. In these cases, the range isn’t exactly endless but just ends when the conditions of the chained methods are satisfied. Let’s look at an example:
p (1..).step(5).take(100)
# [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, …. 491, 496]
What’s happening here? The step(5)
method takes each fifth number from the range, which remains infinite. The take
method takes the first 100 elements of that sequence, which now becomes finite. Using an endless range to begin the expression ensures that the latter part will work for any set of inputs. We could change the 5 or 100 to larger numbers and still get the expected result.
Tip: Ranges work with letters, too:
p ('A'..'Z').step(2)
# ["A", "C", "E", "G", "I", "K", "M", "O", "Q", "S", "U", "W", "Y"]
The lazy
keyword
This one isn’t new to Ruby 2.6, but it is powerful and underused. Enumerables support a form of lazy iteration that helps you avoid reading entire files or sets of database records into memory when you might not need to. For example, when you only need to find the first 10 lines in a file that contain the word “jane”.
File.open("names.txt") do |f|
f.each_line.lazy.select { |line|
line.match(/jane/i)
}.first(10)
end
Although it’s only one extra method, the addition of lazy
after each_line
changes what happens under the hood. The entire file isn’t read into memory, as is what would happen without the lazy keyword. With lazy and first(10)
at the end of the expression, the file is read line by line, but the reading stops as soon as 10 occurrences are found.
Range stepping
The step
method on a range can help you produce a subrange of every 2nd, 3rd, or nth element. Ruby 2.6 brings a bit more power and a new, shorter syntax for stepping.
First off, a new alias for step is available - %
:
p ((1..10) % 2)
# [1, 3, 5, 7, 9]
Next, there are now first
and last
methods that can be called on steps of ranges, which is of type ArithmethicSequence.
p ((1..10) % 2).last
# 9
each_cons
When you need to iterate an array of multiple overlapping elements at a time, each_cons
comes in handy. This method produces sub-arrays from consecutive elements similar to slice. However, the first element of the next array is the second element of the previous. This is easiest to see with an example.
p (1..10).each_cons(2)
# [[1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]
each_cons
takes an integer argument for how large each array should be.
Because strings are just arrays of characters, each_cons
can be used to do some string processing that might otherwise be tedious. Here’s a fun example to make your strings look extra spooky:
str = "arrays and collections are scary"
p str.chars.each_cons(2).map(&:join).join
# "arrrraayyss aanndd ccoolllleeccttiioonnss aarree ssccaarry"
Another, perhaps more useful way to use each_cons
is to keep the previous and next element in context while iterating over a collection.
primes = [2, 3, 5, 7, 11, 13]
primes.each_cons(3).each { |previous, current, next_|
p "#{current} is the prime number between #{previous} and #{next_}"
}
# "3 is the prime number between 2 and 5"
# "5 is the prime number between 3 and 7"
# "7 is the prime number between 5 and 11"
# "11 is the prime number between 7 and 13"
You can see another handy Ruby feature happening here: argument destructuring. One array is being passed to the function argument of the each
function, but we can provide three parameters: previous
, current
, and next_
to have Ruby put the 0th, 1st, and 2nd array elements into those variables automatically.
Conclusion
We’ve just scratched the surface of what’s possible with Ruby Enumerables. Hopefully you’ve learned a few tips to tighten up your current or next codebase. Combining endless ranges, stepping, and the lazy keyword can make an entire family of loop processing use cases much easier.
If you’re interested in learning more about how to level up your Ruby code, I highly recommend the Code[ish] Podcast with Aaron Patterson and this episode about Ruby Regexes and How open source developers will make your company stronger.
Thanks for reading!
Top comments (5)
Enumerable#lazy
probably deserves its own article. It's a great feature if you want your code to be nice to read but still somewhat performant.Ya this is the one that was new to me from the list! Would love to read an article on it alone
Great article. Thanks for sharing this knowledge 🙏
While describing the behaviour of
each_cons
, you mention the above. I think that may not be 100% accurate in all cases, as the first element of the next array is actually the second element of the previous.It just so happens to be correct for the accompanying example because the returned arrays are of size 2.
Hope that makes sense 🙂
Thanks for reading!
Good catch there 😅 I've updated the post to state that correctly, thank you for pointing it out.
Thanks for sharing. We have endless ranges at last! No more
Float::INFINITY
-ish solutions.