4 Lesser Known Ways to Use Ruby’s Enumerable Module
In this post, I’ll show you a few capabilities recently added to Ruby’s Enumerable as well as a few old favorites.
Join the DZone community and get the full member experience.
Join For FreeA 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, and sliced and diced in various ways. This module’s flexibility leads to a 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 enumerables. 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 second, third, 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!
Opinions expressed by DZone contributors are their own.
Trending
-
Microservices With Apache Camel and Quarkus
-
Observability Architecture: Financial Payments Introduction
-
What ChatGPT Needs Is Context
-
Extending Java APIs: Add Missing Features Without the Hassle
Comments