cycle through the rest of Enumerator in Ruby
Asked Answered
T

2

7

I have an Enumerator in the middle of some sequence:

enum = (1..9).each
first = enum.next
second = enum.next

Now I want to cycle through the rest of the sequence (3..9 numbers). But seeming obvious solution (which works in python, e.g.), restart from the beginning of the sequence instead of the third item:

for item in enum
    puts item
end
# prints 1..9 instead of 3..9

Working solution which I found seems ugly:

begin
    while item=enum.next
        puts item
    end
rescue StopIteration
end

So the question: is there a nicer rubyish solution to do this thing? And why does for loop in Ruby act this way?

Thornberry answered 28/4, 2018 at 9:46 Comment(0)
A
11

To answer your question directly, your current code is on the right lines but over-engineered. You only need to do:

enum = (1..9).each
first = enum.next
second = enum.next

loop { puts enum.next }

The loop will break as soon as the enum reaches its end; the loop automatically rescues StopIteration for you. It will only be re-raised if you call enum.next again, after the loop, which doesn't happen here.

However as pointed out by @mudasobwa, using enums like this quite ususual; more common would be to use (1..9).each_with_index and explicitly handle the first yields by their index.

Angola answered 28/4, 2018 at 13:43 Comment(3)
Wow, indeed, rescue is redundant, thanks, never knew that (which in turn means the whole construct is as unrubyish that I failed to try it in 5+ years experience :)Delorenzo
@mudasobwa From the docs: "StopIteration - Raised to stop the iteration, in particular by Enumerator#next. It is rescued by Kernel#loop." But yes, I had to double-check too, despite having a similar amount of experience; it's very rare to see enum.next in ruby code!Angola
Don't undervalue enum.next! One example: n.public_send(enum.next, m), where enum = [:+, :- ].cycle.Gaeta
D
3

In ruby you are strongly encouraged not to use for, while and until unless you do perfectly know what and why you are doing.

I could hardly imagine the real problem when one needs to do something special on first two elements of Enumerable (this possibly means an architectural issue,) but instead of this:

enum.next
enum.next
loop do
  begin
    puts enum.next
  rescue StopIteration
    break 
  end
end

Update: rescue is not needed there, see @TomLord’s answer for clarification.

You’d better do this:

enum.with_index do |element, idx|
  print "First " if idx == 0
  print "Second " if idx == 1
  puts element
end

Iterator’s internals almost never should be called explicitly, use efficient and idiomatic iteration, mapping and reducing instead.

Delorenzo answered 28/4, 2018 at 10:7 Comment(6)
with_index is in class Enumerator, If so, why enum.each.with_index why not enum.with_index ?Wayless
@Wayless to trigger the iteration. enum.with_index returns an enumerator.Delorenzo
p enum.each.with_index.class returns enumerator as well.Wayless
@Wayless ah, indeed, you are certainly right, thanks. Updated the answer.Delorenzo
@Rajagopalan, case is important: "enum.each.with_index.class" returns the class Enumerator, not an "enumerator". Also, p at the beginning prints "Enumerator"as opposed to returning Enumerator. Be precise!Gaeta
@CarySwoveland No, I did not understand you. both are returning the object of enumerator class, eh?Wayless

© 2022 - 2024 — McMap. All rights reserved.