Ruby 3.1 adds Enumerable#compact and Enumerator::Lazy#compact


Enumerable mixin in Ruby provides several searching and sorting methods to classes. The Enumerable module is useful in classes where there are collections of data that might require sorting or searching collections.

However #compact method was not available in Enumerable even though classes like Array already has the #compact method.

Ruby 3.1 adds #compact directly to the Enumerable module and Enumerator::Lazy class.

To understand the use case of Enumerable, let’s assume we have a Reviews class with the reviews attribute. Few reviews may be empty. Now let’s assume we want the first five non-empty reviews.

The Ruby implementation of this Reviews class will be something like this.

# reviews.rb
class Reviews
  include Enumerable

  def initialize(reviews = Array.new)
    @reviews = reviews
  end

  def each &block
    @reviews.each { |review| block.call(review) }
  end
end

Our solution for this would vary based on our Ruby version.

Before Ruby 3.1

Before Ruby 3.1, considering the above example if we had to delete nil reviews from the collection class, we would have to use the #reject method as shown below:

reviews = Reviews.new(["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound"])
# => #<Reviews:0x00007fb77f0548d8 @reviews=["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound"]>

reviews.reject { |review| review.nil? }.first(5)
# => ["Nice touchscreen", "Excellent", "Very good", "Nice sound"]

After Ruby 3.1

After the introduction of #compact in the Enumerable module, we can call compact on the class instance itself.

reviews = Reviews.new(["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound"])
# => #<Reviews:0x00007fb77f0548d8 @reviews=["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound"]>

reviews.compact.first(5)
# => ["Nice touchscreen", "Excellent", "Very good", "Nice sound"]

Ruby 3.1 also adds #compact to Enumerable::Lazy class. Now Enumerator::Lazy classes can chain #compact to return only non-nil values.

Now let’s imagine our Reviews class has more than 1000 objects, our compact.first(5) method will go through the entire 1000 reviews and select only the first 5.

This issue can be optimized using Enumerable::Lazy class.

reviews = Reviews.new(["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound", ...])
# => #<Reviews:0x00007fb77f0548d8 @reviews=["Nice touchscreen", "Excellent", nil, "Very good", nil, "Nice sound", ...]>

reviews.compact.first(5)
# => ["Nice touchscreen", "Excellent", "Very good", "Nice sound"]

The lazy method is fetching items one-by-one until five items are fetched instead of iterating over 1000 items at once.