Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 551
Stefan Avatar asked Jan 01 '23 00:01

Stefan


2 Answers

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.

like image 182
kgilpin Avatar answered Feb 01 '23 23:02

kgilpin


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]
like image 21
Kache Avatar answered Feb 01 '23 23:02

Kache