How to create a “clone”-able enumerator for external iteration?

梦想的初衷 提交于 2020-08-07 05:35:00

问题


I want to create an enumerator for external iteration via next that is clone-able, so that the clone retains the current enumeration state.

As an example, let's say I have a method that returns an enumerator which yields square numbers:

def square_numbers
  return enum_for(__method__) unless block_given?

  n = d = 1
  loop do
     yield n
     d += 2
     n += d
   end
end

square_numbers.take(10)
#=> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

And I want to enumerate the first 5 square numbers, and for each value, print the subsequent 3 square numbers. Something that's trivial with each_cons:

square_numbers.take(8).each_cons(4) do |a, *rest|
  printf("%2d: %2d %2d %2d\n", a, *rest)
end

Output:

 1:  4  9 16
 4:  9 16 25
 9: 16 25 36
16: 25 36 49
25: 36 49 64

But unlike the above, I want to use external iteration using two nested loops along with next and clone:

outer_enum = square_numbers
5.times do
  i = outer_enum.next
  printf('%2d:', i)

  inner_enum = outer_enum.clone
  3.times do
    j = inner_enum.next
    printf(' %2d', j)
  end
  print("\n")
end

Unfortunately, the above attempt to clone raises a:

`initialize_copy': can't copy execution context (TypeError)

I understand that Ruby doesn't provide this out-of-the-box. But how can I implement it myself? How can I create an Enumerator that supports clone?

I assume that it's a matter of implementing initialize_copy and copying the two variable values for n and d, but I don't know how or where to do it.


回答1:


Ruby fibers cannot be copied, and the C implementation of Enumerator stores a pointer to a fiber which does not appear to be exposed to Ruby code in any way.

https://github.com/ruby/ruby/blob/752041ca11c7e08dd14b8efe063df06114a9660f/enumerator.c#L505

if (ptr0->fib) {
    /* Fibers cannot be copied */
    rb_raise(rb_eTypeError, "can't copy execution context");
}

Looking through the C source, it's apparent that Enumerators and Fibers are connected in a pretty profound way. So I doubt that there is any way to change the behavior of initialize_copy to permit clone.




回答2:


Perhaps you could just write a class of your own that does what you ask:

class NumberSquarer
  def initialize
    @n = @d = 1
  end

  def next
    ret = @n
    @d += 2
    @n += @d
    ret
  end
end

ns1 = NumberSquarer.new
Array.new(5) { ns1.next }
# => [1, 4, 9, 16, 25]

ns2 = ns1.clone
Array.new(5) { ns2.next }
# => [36, 49, 64, 81, 100]



回答3:


Disclaimer: I'm answering my own question


One way to achieve this is by sub-classing Enumerator. In particular, the now-deprecated variant that takes an object and a method:

class ObjectEnumerator < Enumerator
  attr_reader :object, :method

  def initialize(object, method = :each)
    @object = object
    @method = method
    super
  end

  def initialize_copy(orig)
    initialize(orig.object.clone, orig.method)
  end
end

That orig.object.clone above is where the magic happens: it clones the object we are traversing.

In addition, we need such clone-able object. A simple way is to have a class which holds the state as instance variables: (shamelessly copied from Kache's answer)

class NumberSquarer
  def initialize
    @d = -1
    @n = 0
  end

  def each
    return ObjectEnumerator.new(self, __method__) unless block_given?

    loop do
      @d += 2
      @n += @d  #    had to be reordered b/c
      yield @n  # <- yield has to come last
    end
  end
end

This gives us a basic, clone-able enumerator:

e = NumberSquarer.new.each
#=> #<ObjectEnumerator: #<NumberSquarer:0x00007fde60915e10 @d=-1, @n=0>:each>

e.next #=> 1
e.next #=> 4

other = enum.clone
#=> #<ObjectEnumerator: #<NumberSquarer:0x00007fcf23842520 @d=3, @n=4>:each>

enum.next #=> 9
enum.next #=> 16

other.next #=> 9



回答4:


I'm providing a different solution that is not a straight answer to the question:

How can I create an Enumerator that supports clone?

But if I'm not wrong the only purpose of cloning the not clonable Ruby's Enumerator is to get a reference to the next object in the enumerator.

In this case, we need both values stored in odd_sum and square in the example below.

We can store those values in an Array and return the array instead of a single value, then we can use Enumerator.peek in order to have the array that is used to initialize a new Enumerator.

def square_numbers(starters = {})
  return enum_for(__method__, starters) unless block_given?

  last_odd = starters.fetch(:square_odd, [1,1])[1]
  square = starters.fetch(:square_odd, [1,1])[0]

  loop do
     yield [square, last_odd]
     last_odd += 2
     square += last_odd
   end
end

outer_enum = square_numbers
5.times do
  i = outer_enum.next[0]
  printf('%2d:', i)

  inner_enum = square_numbers(square_odd: outer_enum.peek)
  3.times do
    j = inner_enum.next[0]
    printf(' %2d', j)
  end
  print("\n")
end


来源:https://stackoverflow.com/questions/62637661/how-to-create-a-clone-able-enumerator-for-external-iteration

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!