Zed
Zed

Reputation: 5921

Enumerators in Ruby

I'm having a trouble understanding Enumerators in Ruby.

Please correct me If I'm wrong, o.enum_for(:arg) method is supposed to convert object to Enumerator and every iteration over object o should call arg method? What confuses me is how this line of code works

[4, 1, 2, 0].enum_for(:count).each_with_index do |elem, index|
  elem == index
end

It should count how many elements are equal to their position in the array, and it works. However, I don't understand what's actually going on. Is each_with_index calling count method on every iteration? If someone could explain, it would be great.

Upvotes: 3

Views: 567

Answers (2)

zetetic
zetetic

Reputation: 47578

From Programming Ruby:

count enum.count {| obj | block } → int

Returns the count of objects in enum that equal obj or for which the block returns a true value.

So the enumerator visits each element and adds 1 to the count if the block returns true, which in this case means if the element is equal to the array index.

I haven't seen this pattern used much (if at all)—I would be more inclined to:

[4, 1, 2, 0].each_with_index.select { |elem, index| elem == index }.count

EDIT

Lets take a look at the example from your comment:

[4, 1, 2, 0].enum_for(:each_slice, 2).map do |a, b|
  a + b
end

each_slice(2) takes the array 2 elements at a time and returns an array for each slice:

[4, 1, 2, 0].each_slice(2).map # => [[4,1], [2,0]]

calling map on the result lets us operate on each sub-array, passing it into a block:

[4, 1, 2, 0].enum_for(:each_slice, 2).map do |a,b|
  puts "#{a.inspect} #{b.inspect}"
end

results in

4 1
2 0

a and b get their values by virtue of the block arguments being "splatted":

a, b = *[4, 1]
a # => 4
b # => 1

You could also take the array slice as the argument instead:

[4, 1, 2, 0].enum_for(:each_slice, 2).map {|a| puts "#{a.inspect}"}

[4, 1]
[2, 0]

Which lets you do this:

[4, 1, 2, 0].enum_for(:each_slice, 2).map {|a| a.inject(:+) } #=> [5,2]

Or if you have ActiveSupport (i.e. a Rails app),

[4, 1, 2, 0].enum_for(:each_slice, 2).map {|a| a.sum }

Which seems a lot clearer to me than the original example.

Upvotes: 3

Zach Kemp
Zach Kemp

Reputation: 11904

array.count can normally take a block, but on its own, it returns a fixnum, so it can't be chained to .with_index the way some other iterators can (try array.map.with_index {|x,i ... }, etc).

.enum_for(:count) converts it into a enumerator, which allows that chaining to take place. It iterates once over the members of array, and keeps a tally of how many of them equal their indexes. So count is really only being called once, but only after converting the array into something more flexible.

Upvotes: 0

Related Questions