Enumerating collections the Ruby way
Enumeration is the complete and ordered listing/traversing of all items in a collection. To list over something in Ruby, most of us may start with something like this:
class Collection
def initialize(items)
@items = items
end
def each(&block)
@items.each { |item| block.(item) }
end
# OR
def each(&block)
@items.each(&block)
end
end
A good follow question will be can we now use the #map
and #reduce
that we all love? We will be horrified to learn that we need to implement them by hand if we follow the way above!
Fortunately, we can implement our own with the Enumerable
module, as do by the Hash
, Array
and Range
classes. By including the Enumerable
module in our collection class and implementing the #each
method, we will get access to all methods that has an element of listing over elements such as #map
, #reduce
and #each_slice
.
Example
There are two booths in a data collection exercise. The first collects a person's name and the second collects the person's hobbies. After filling up the entire list, the organizer wants to traverse over a person's name and hobbies together. An array of names and a two-dimensional array of hobbies are readied in the same sequence. We can imagine these requirements as a NameHobbiesColletion
class to list over a person's name and hobbies in sequence.
class NameHobbiesCollection
include Enumerable
def initialize(names, array_of_hobbies)
@names = names
@array_of_hobbies = array_of_hobbies
end
# This will be the only method we need to implement for traversal.
def each(&block)
@names.zip(@array_of_hobbies).each do |name, hobbies|
block.(name, hobbies)
end
end
end
Testing Them Out
-
Let's setup an instance and see some enumeration methods in action!
collection = NameHobbiesCollection.new( %w(James Joe Tony), [ %w(Talking Baking), %w(Science Coding Talking), %w(Coffee) ] )
-
We can use
#each
to list over the collecitons and do things:collection.each do |name, hobbies| puts "#{name} has hobbies of #{hobbies.join(', ')}" end # => James has hobbies of Taking, Baking # => Joe has hobbies of Science, Coding, Talking # => Tony has hobbies of Coffee
-
We can use
#map
to produce an array of human-readable logs:collection.map do |name, hobbies| "name:#{name} hobbies:[#{hobbies.join(',')}]" end # => [ # => "name:James hobbies:[Talking,Baking]", # => "name:Joe hobbies:[Science,Coding,Talking]", # => "name:Tony hobbies:[Coffee]" # => ]
-
#partition
allows us to group chatty people together and the others together:collection.partition do |_name, hobbies| hobbies.include? 'Talking' end # => [ # => [["James", ["Talking", "Baking"]], ["Joe", ["Science", "Coding", "Talking"]]], # => [["Tony", ["Coffee"]]] # => ]
-
We can use
#each_slice
to iterate over the collection in chunks, useful for persisting items to the database in batches instead of everything at once:collection.each_slice(2).to_a # => [ # => [["James", ["Talking", "Baking"]], ["Joe", ["Science", "Coding", "Talking"]]], # => [["Tony", ["Coffee"]]] # => ]
Can we make it chainable?
If we want to chain methods like collection.map.with_index
, the colletion is expected to return an Enumerator
. To do that, we can rely on the Object#to_enum
method to create an enumerator listing over the collection using a specified method from it. Any arguments should be past over.
def each(*args, &block)
# When there is no block given, we return an Enumerator that points to this each method!
return to_enum(__method__, *args) unless block_given?
# Implementation follows here when there is a block.
end
Testing them out
-
Chaining
#each
with#with_index
:collection.each.with_index do |(name, hobbies), index| puts "queue #{index}: #{name} has hobbies of #{hobbies.join(', ')}" end # => queue 0: James has hobbies of Talking, Baking # => queue 1: Joe has hobbies of Science, Coding, Taking # => queue 2: Tony has hobbies of Coffee
-
Iterating over the collection with an arbitrary object with
#each
and#with_object
and returns it:collection.each.with_object({}) do |(name, hobbies), hash| hash[name] = hobbies end # => { # => "James"=>["Talking", "Baking"], # => "Joe"=>["Science", "Coding", "Talking"], # => "Tony"=>["Coffee"] # => }
-
Emitting the entire collections:
collection.each.entries collection.each.to_a # => [ # => ["James", ["Talking", "Baking"]], # => ["Joe", ["Science", "Coding", "Talking"]], # => ["Tony", ["Coffee"]] # => ]
References
Enumerator
includes the Enumerable
module. Explore more methods availbale in the Enumerator
module below:
- Enumerable: https://ruby-doc.org/core-3.0.2/Enumerable.html
- Enumerator: https://ruby-doc.org/core-3.0.3/Enumerator.html