Annotated notes from reading on Enumerators of chapter ten (Collections central: Enumerable and Enumerator) of The Well-Grounded Rubyist by David A. Black.
An Enumerator is an object with an each method that is used as an Enumerable object.
The difference between an Enumerator and an Iterator is that an Enumerator is an enumerable object that can maintain state and therefore remembers where it is in the enumeration.
Enumerator.new
e = Enumerator.new do |y|
puts "START"
(1..3).each {|i| y << i}
puts "END"
end
p e.map {|n| n * 10}
Output:
START
END
[10, 20, 30]
Note: y is an instance of Enumerator::Yielder and is automatically passed to the code block.
The above is equivalent to:
e = Enumerator.new do |y|
y << 1
y << 2
y << 3
end
Enumerators can also be instantiated by blockless iterator calls:
e = "Hello".each_char
p e
e.each {|c| print c}
Output:
#<Enumerator: "Hello":each_char>
Hello
Enumerators can ‘attached’ themselves to other objects by specifying an enumerable method to hook up to. When an Enumerator each (or other enumerable) method is called, it will get the values to return by triggering the next yield from the given enumerable method of the object it is attached to.
countries = %w{Denmark Finland France Spain UK}
e = countries.enum_for(:select)
p e.each {|c| c.size > 5}
You can also pass arguments to the enum_for method if they are expected by the object’s target method itself:
numbers = [1, 2, 3, 4]
e = numbers.enum_for(:inject, 10)
e.each {|acc, n| acc + n}
>> 20
Using Enumerators to protect collection from change
If a method is passed a collection directly, the method is able to add or remove elements from the collection without problem.If however we want to prevent anyone from modifying the original collection, we can pass on an Enumerator which will allow for standard enumerable operations to take place (querying, filtering, etc.) but not change the collection.
Accessing a collection using an Enumerator and not through the collection itself protects the collection from change.
Here we have access directly to the internal collection and modify it:
class Countries
def names
@names ||= %w{France Denmark}
end
end
countries = Countries.new
countries.names << "UK"
p countries.names
>> ["France", "Denmark", "UK"]
If we try to do the same using the Enumerator, we get an error:
class Countries
def names
@names ||= %w{France Denmark}
end
def immutable_names
# Equivalent to:
# Enumerator.new(names)
self.names.to_enum
end
end
countries = Countries.new
countries.immutable_names << "UK"
Output:
undefined method `<<' for #<Enumerator: nil:each> (NoMethodError)
But we can still however iterate through the collection:
class Countries
def names
@names ||= %w{France Denmark}
end
def immutable_names
self.names.to_enum
end
end
countries = Countries.new
countries.immutable_names.each {|c| puts c.upcase}
Output:
FRANCE
DENMARK
Enumerator#next and Enumerator#rewind
You can move through the collection going forward or backwards using the next and rewind methods respectively.
names ||= %w{France Denmark UK Spain}
e = names.to_enum
puts e.next
puts e.next
puts e.rewind
puts e.next
Output:
France
Denmark
#<Enumerator:0x757ec0>
France
Adding enumerability
class Countries
NAMES = %w{France UK}
def list(str)
NAMES.each {|c| yield str + c}
end
end
countries = Countries.new
e = countries.enum_for(:list, "Name: ")
puts e.select {|c| c}
Output:
Name: France
Name: UK
Method chaining
countries = %w{France UK Spain Denmark}
puts countries.select {|c| c.size > 5}.map(&:upcase).join(", ")
Output:
FRANCE, DENMARK
Enumerator#with_index
countries = %w{France UK Spain Denmark}
p countries.map.with_index {|c, i| "#{i}: #{c}"}
Output:
["0: France", "1: UK", "2: Spain", "3: Denmark"]