DEV Community

Cover image for 4 lesser known ways to use Ruby’s Enumerable module
Josh Dzielak 🔆 for Orbit

Posted on • Edited on

4 lesser known ways to use Ruby’s Enumerable module

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
# ...

Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

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.

Collapse
 
coreyja profile image
Corey Alexander

Ya this is the one that was new to me from the list! Would love to read an article on it alone

Collapse
 
sannim1 profile image
Abdulmusawwir Sanni

Great article. Thanks for sharing this knowledge 🙏


However, the first element of the next array is the last element of the previous

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 🙂

Collapse
 
joshed profile image
Josh Dzielak 🔆

Thanks for reading!

Good catch there 😅 I've updated the post to state that correctly, thank you for pointing it out.

Collapse
 
vinibrsl profile image
Vinicius Brasil

Thanks for sharing. We have endless ranges at last! No more Float::INFINITY-ish solutions.