How to use an enumerator
Asked Answered
P

6

8

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?

Pore answered 5/12, 2013 at 8:26 Comment(3)
"so well thought out!" – I really like Ruby, it is my favorite language, but that is not an attribute I would ever ascribe to Ruby :-DPatrilocal
@JörgWMittag, I'm so happy you dropped by. I really like your answers on this forum. I even told my wife about "this guy from Germany". Have you written any books?Pore
@Pore Yes Jörg is really one of my favorite answerer too.. :)Institutionalize
C
4

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)

Cryptonym answered 5/12, 2013 at 9:51 Comment(1)
Jörg, please consider writing a book. I'm sure there are subjects that are not properly covered yet. Like David Black and Russ Olsen, you have a gift for explaining which, when combined with a deep understanding of the material, can be very helpful to others.Pore
J
9

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"]
Jarry answered 5/12, 2013 at 8:35 Comment(4)
vgoff, very good answer. But you said "We don't want to process it now, because it means we may need to create another later". I don't understand why it's not processed now, and why not just use an array.Pore
If we simply use the enumerable right at that point, then I would have to repeat my .map command. This way I do the map command once, and I can use the Enumerator more than just that one time. This is the benefit of being able to get an Enumerator back. We can use that one Enumerator instance over and over again. We don't have to duplicate that work.Jarry
So is it more efficient as far as the machine is concerned? Does Ruby work less when I iterate over an enumerator twice rather than an array twice? or do I work less? or both?Pore
If you create something once, it is done. If you create the same thing more than once, then it is that many times the work.Jarry
R
4

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]]
Rugen answered 5/12, 2013 at 8:51 Comment(1)
Hirolau, this is a very interesting answer. I find the level of abstraction a little difficult to get my mind around at this point. I'll be happy if you explain a little more but even if you don't, at least I've learned that you can chain methods this way and I'll try and chew on it some time.Pore
C
4

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)

Cryptonym answered 5/12, 2013 at 9:51 Comment(1)
Jörg, please consider writing a book. I'm sure there are subjects that are not properly covered yet. Like David Black and Russ Olsen, you have a gift for explaining which, when combined with a deep understanding of the material, can be very helpful to others.Pore
F
2

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.

Farleigh answered 5/12, 2013 at 9:38 Comment(0)
M
0

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
Mansion answered 5/12, 2013 at 8:34 Comment(4)
Decent example, but then you also need to know that you are waiting for an exception to happen if you exceed the bounds of the enumerator.Jarry
it just a simple example, i dont use this technicMansion
Thanks Monk_Code, but I figured out you'd probably want me to do map = array.map and then use map.next, right?Pore
in this example enumerator(array.map) its like freezy iterator you can pass each step without block.Mansion
A
0

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]
Antinomy answered 5/12, 2013 at 8:48 Comment(2)
Thank bro. But again, why an Enumerator and not just an Array?Pore
Just because you might implement your own Enumerator. And because there are Kernel#to_enum and Kernel#enum_for helpers. Enumerator is more general, than Array: take a look at examples here.Antinomy

© 2022 - 2024 — McMap. All rights reserved.