7
votes

I have a tree that I'm trying to traverse. As I traverse it, I keep a stack of enumerators in which each enumerator is used to enumerate over a tree's children.

I'd like to be able to duplicate this stack of enumerators and hand it to another object so it may traverse the tree starting in the place indicated by the state of the stack.

When I attempt to call #dup on Enumerator, I get an error. Is it possible to duplicate an Enumerator? If not, how could I accomplish the same thing? (I've considered a stack of integers as indices, but am worried about the efficiency.

Here's some code to show what I'm seeing...

Once the first enumerator has started, you cannot duplicate it. That is my situation.

a = [1,2,3].each
 => #<Enumerator: [1, 2, 3]:each> 
a.next
 => 1 
b = a.dup
TypeError: can't copy execution context
    from (irb):3:in `initialize_copy'
    from (irb):3:in `initialize_dup'
    from (irb):3:in `dup'
    from (irb):3
3
Call @dup? You mean call dup? - sawa
Is this an academic exercise or would you benefit from the RubyTree gem? - Mark Thomas
#next is not preferred way to use enumerators in ruby. I believe it is defined using fibers, and stackframes cannot be duplicated. Can you elaborate what do you need this for? Maybe we could suggest more ruby-way of solving your problem. - samuil
@samuil: #next is the preferred way to do external iteration. And yes, enumerators can be duplicated before they are iterated. - Catnapper
Are you trying to do a depth first tree traversal with numerators? - Candide

3 Answers

1
votes

Implement your own enumerator class.

There’s not much magic to an enumerator beyond incrementing an internal counter.

class MyArrayEnumerator
  def initialize(array)
    @ary,@n=array,0
  end
  def next
    raise StopIteration if @n == @ary.length
    a=@ary[@n];@n+=1;a
  end
end

class Array
  def my_each
    MyArrayEnumerator.new(self)
  end
end

a = [1,2,3].my_each # => #<MyArrayEnumerator:0x101c96588 @n=0, @array=[1, 2, 3]>
a.next # => 1
b = a.dup # => #<MyArrayEnumerator:0x101c95ae8 @n=1, @array=[1, 2, 3]>
a.next # => 2
b.next # => 2
1
votes

Use clone instead:

e1 = [1,2,3].each
e1.dup # TypeError: can't copy execution context
e2 = e1.clone
e1.next #=> 1
e2.next #=> 1
0
votes

keep one "head" amongst Enumerator's instances, and store history for behind copies:

class Enum

  def initialize()
    @history = [] # history will be shared between instances
    @history_cursor = -1
    @head = Enumerator.new do |yielder|
      @yielder = yielder
      enumerate
    end
  end

  def next
    if @history_cursor < @history.count - 1
      @history[@history_cursor += 1]
    else
      new_item @head.next
    end
  end

  private

  def new_item item
    @history << item
    @history_cursor = @history.count - 1
    item
  end

  def enumerate
    13.times do |i|
      @yielder << i # yielder is shared between instances
    end
  end

end

Usage:

enum1 = Enum.new
p enum1.next # 0
enum2 = enum1.clone
p enum2.next # 1
p enum1.next # 1