How to use an enumerator

1.4k views Asked by At

In the Ruby Array Class documentation, I often find:

If no block is given, an enumerator is returned instead.

Why would I not pass a block to #map? What would be the use of my doing just:

[1,2,3,4].map

instead of doing:

[1,2,3,4].map{ |e| e * 10 } # => [10, 20, 30, 40]

Can someone show me a very practical example of using this enumerator?

6

There are 6 answers

1
Jörg W Mittag On BEST ANSWER

The main distinction between an Enumerator and most other data structures in the Ruby core library (Array, Hash) and standard library (Set, SortedSet) is that an Enumerator can be infinite. You cannot have an Array of all even numbers or a stream of zeroes or all prime numbers, but you can definitely have such an Enumerator:

evens = Enumerator.new do |y|
  i = -2
  y << i += 2 while true
end

evens.take(10)
# => [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

zeroes = [0].cycle

zeroes.take(10)
# => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

So, what can you do with such an Enumerator? Well, three things, basically.

  1. Enumerator mixes in Enumerable. Therefore, you can use all Enumerable methods such as map, inject, all?, any?, none?, select, reject and so forth. Just be aware that an Enumerator may be infinite whereas map returns an Array, so trying to map an infinite Enumerator may create an infinitely large Array and take an infinite amount of time.

  2. There are wrapping methods which somehow "enrich" an Enumerator and return a new Enumerator. For example, Enumerator#with_index adds a "loop counter" to the block and Enumerator#with_object adds a memo object.

  3. You can use an Enumerator just like you would use it in other languages for external iteration by using the Enumerator#next method which will give you either the next value (and move the Enumerator forward) or raise a StopIteration exception if the Enumerator is finite and you have reached the end.

Eg., an infinite range: (1..1.0/0)

0
sawa On

In addition to hirolau's answer, there is another method lazy that you can combine to modify the enumerator.

a_very_long_array.map.lazy

If map always took a block, then there would have to be another method like map_lazy, and similarly for other iterators to do the same thing.

4
Roman Kiselenko On

Enumerator A class which allows both internal and external iteration

=> array = [1,2,3,4,5]
=> array.map
=> #<Enumerator: [2, 4, 6, 8, 10]:map>
=> array.map.next
=> 2
=> array.map.next_values
=> [0] 2
4
vgoff On

Good question.

What if we want to do multiple things with the enumerator that is created? We don't want to process it now, because it means we may need to create another later?

my_enum = %w[now is the time for all good elves to get to work].map # => #<Enumerator: ["now", "is", "the", "time", "for", "all", "good", "elves", "to", "get", "to", "work"]:map>

my_enum.each(&:upcase) # => ["NOW", "IS", "THE", "TIME", "FOR", "ALL", "GOOD", "ELVES", "TO", "GET", "TO", "WORK"]
my_enum.each(&:capitalize) # => ["Now", "Is", "The", "Time", "For", "All", "Good", "Elves", "To", "Get", "To", "Work"]
2
Aleksei Matiushkin On

I guess sometimes you want to pass the Enumerator to another method, despite where this Enumerator came from: map, slice, whatever:

def report enum
  if Enumerator === enum
    enum.each { |e| puts "Element: #{e}" }
  else
    raise "report method requires enumerator as parameter"
  end
end

> report %w[one two three].map
# Element: one
# Element: two
# Element: three

> report (1..10).each_slice(2)    
# Element: [1, 2]
# Element: [3, 4]
# Element: [5, 6]
# Element: [7, 8]
# Element: [9, 10]
1
hirolau On

The feature of returning a enumerable when no block is given is mostly used when chaining functions from the enumerable class together. Like this:

abc = %w[a b c]
p abc.map.with_index{|item, index| [item, index]} #=> [["a", 0], ["b", 1], ["c", 2]]

edit:

I think my own understanding of this behavior is a bit too limited in order to give a proper understanding of the inner workings of Ruby. I think the most important thing to note is that arguments are passed on in the same way they are for Procs. Thus if an array is passed in, it will be automatically 'splatted' (any better word for this?). I think the best way to get an understanding is to just use some simple functions returning enumerables and start experimenting.

abc = %w[a b c d]
p abc.each_slice(2)                #<Enumerator: ["a", "b", "c", "d"]:each_slice(2)>
p abc.each_slice(2).to_a           #=> [["a", "b"], ["c", "d"]]
p abc.each_slice(2).map{|x| x}     #=> [["a", "b"], ["c", "d"]]
p abc.each_slice(2).map{|x,y| x+y} #=> ["ab", "cd"]
p abc.each_slice(2).map{|x,| x}    #=> ["a", "c"] # rest of arguments discarded because of comma.
p abc.each_slice(2).map.with_index{|array, index| [array, index]} #=> [[["a", "b"], 0], [["c", "d"], 1]]
p abc.each_slice(2).map.with_index{|(x,y), index| [x,y, index]}   #=> [["a", "b", 0], ["c", "d", 1]]