It's time for another episode of Ruby Magic! This time, we'll look at one of Ruby's most magical features, which provides most of the methods you'll use when working with Ruby's enumerable classes like Array
, Hash
and Range
. In the process, we'll learn what you can do with enumerable objects, how enumeration works, and how to make an object enumerable by implementing a single method.
Enumerable
, #each
and Enumerator
Enumeration refers to traversing over objects. In Ruby, we call an object enumerable when it describes a set of items and a method to loop over each of them.
The built-in enumerables get their enumeration features by including the Enumerable
module, which provides methods like #include?
, #count
, #map
, #select
and #uniq
, amongst others. Most of the methods associated with arrays and hashes aren't actually implemented in these classes themselves, they're included.
Note: Some methods, like #count
and #take
on the Array
class, are implemented specifically for arrays instead of using the ones from the Enumerable
module. That's usually done to make operation faster.
The Enumerable
module relies on a method named #each
, which needs to be implemented in any class it's included in. When called with a block on an array, the #each
method will execute the block for each of the array's elements.
irb> [1,2,3].each { |i| puts "* #{i}" }
* 1
* 2
* 3
=> [1,2,3]
If we call the #each
method on an array without passing a block to execute for each of its elements, we'll receive an instance of Enumerator
.
irb> [1,2,3].each
=> #<Enumerator: [1, 2, 3]:each>
Instances of Enumerator
describe how to iterate over an object. Enumerators iterate over objects manually and chain enumeration.
irb> %w(dog cat mouse).each.with_index { |a, i| puts "#{a} is at position #{i}" }
dog is at position 0
cat is at position 1
mouse is at position 2
=> ["dog", "cat", "mouse"]
The #with_index
method is a good example of how changed enumerators work. In this example, #each
is called on the array to return an enumerator. Then, #with_index
is called to add indices to each of the array's elements to allow printing each element's index.
Making objects enumerable
Under the hood, methods like #max
, #map
and #take
rely on the #each
method to function.
def max
max = nil
each do |item|
if !max || item > max
max = item
end
end
max
end
Internally, Enumerable
's methods have C implementations, but the example above roughly shows how #max
works. By using #each
to loop over all values and remembering the highest, it returns the maximum value.
def map(&block)
new_list = []
each do |item|
new_list << block.call(item)
end
new_list
end
The #map
function calls the passed block with each item and puts the result into a new list to return after looping over all values.
Since all methods in Enumerable
use the #each
method to some extent, our first step in making a custom class enumerable is implementing the #each
method.
Implementing #each
By implementing the #each
function and including the Enumerable
module in a class, it becomes enumerable and receives methods like #min
, #take
and #inject
for free.
Although most situations allow falling back to an existing object like an array and calling the #each
method on that, let's look at an example where we have to write it ourselves from scratch. In this example, we'll implement #each
on a linked list to make it enumerable.
Linked lists: lists without arrays
A linked list is a collection of data elements, in which each element points to the next. Each element in the list has two values, named the head and the tail. The head holds the element’s value, and the tail is a link to the rest of the list.
[42, [12, [73, nil]]
For a linked list with three values (42, 12 and 73), the first element’s head is 42, and the tail is a link to the second element. The second element’s head is 12, and the tail holds the third element. The third element’s head is 73, and the tail is nil
, which indicates the end of the list.
In Ruby, a linked list can be created by using a class that holds two instance variables named @head
and @tail
.
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
end
The #<<
method is used to add new values to the list, which works by returning a new list with the passed value as the head, and the previous list as the tail.
In this example, the #inspect
method is added so we can see into the list to check which elements it contains.
irb> LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
Now that we have a linked list, let's implement #each
on it. The #each
function takes a block and executes it for each value in the object. When implementing it on our linked list, we can use the list's recursive nature to our advantage by calling the passed block on the list's @head
, and calling #each
on the @tail
, if it exists.
class LinkedList
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
When calling #each
on an instance of our linked list, it calls the passed block with current @head
. Then, it calls each on the linked list in @tail
unless the tail is nil
.
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb> list.each { |item| puts item }
42
12
73
=> nil
Now that our linked list responds to #each
, we can include Enumberable
to make our list enumerable.
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
block.call(@head)
@tail.each(&block) if @tail
end
end
irb> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb(main):003:0> list.count
=> 3
irb> list.max
=> 73
irb> list.map { |item| item * item }
=> [1764, 144, 5329]
irb> list.select(&:even?)
=> [42, 12]
Returning Enumerator
instances
We can now loop over all values in our linked list, but we can't chain enumerable functions yet. To do that, we'll need to return an Enumerator
instance when our #each
function is called without a block.
class LinkedList
include Enumerable
def initialize(head, tail = nil)
@head, @tail = head, tail
end
def <<(item)
LinkedList.new(item, self)
end
def inspect
[@head, @tail].inspect
end
def each(&block)
if block_given?
block.call(@head)
@tail.each(&block) if @tail
else
to_enum(:each)
end
end
end
To wrap an object in an enumerator, we call the #to_enum
method on it. We pass :each
, as that's the method the enumerator should be using internally.
Now, calling our #each
method without a block will allow us to chain enumeration.
irb(main):226:0> list = LinkedList.new(73) << 12 << 42
=> [42, [12, [73, nil]]]
irb(main):227:0> list.each
=> #<Enumerator: [42, [12, [73, nil]]]:each>
irb(main):228:0> list.map.with_index.to_h
=> {42=>0, 12=>1, 73=>2}
Nine lines of code and an include
By implementing #each
using the Enumerable
module and returning Enumerator
objects from our own, we were able to supercharge our linked list by adding nine lines of code and an include.
This concludes our overview of enumerables in Ruby. If you liked this article, check out the entire series on Ruby Magic. Magicians never share their secrets. But we do.
We'd love to know what you thought of this article, or if you have any questions. We're always on the lookout for topics to investigate and explain, so if there's anything magical in Ruby you'd like to read about, don't hesitate to leave a comment.
Top comments (2)
Hi, thanks for the Enumarator explanation really liked the article. Just wanted to oint out a typo here:
"...We pass :each, to tell the as that's the method the enumerator..."
I think there is a missing word here. Have a good day.
Great find! In fact, those were extra words! Updated. :)