Ruby中作为无限生成器的枚举器

anhgbhbe  于 2022-11-04  发布在  Ruby
关注(0)|答案(5)|浏览(138)

我正在阅读一个资源,它解释了如何将枚举器用作生成器,例如:

triangular_numbers = Enumerator.new do |yielder|
  number = 0
  count = 1
  loop do
    number += count
    count += 1
    yielder.yield number
  end
end

print triangular_numbers.next, " " 
print triangular_numbers.next, " " 
print triangular_numbers.next, " "

我不明白这里yielder的用途,它取什么值,以及这段代码是如何与程序的其余代码并行执行的。
执行从顶部开始,可能在块向我的代码“生成”一个值时暂停。
有人能解释一下在编译器的眼中这一切是如何执行的吗?

wxclj1h5

wxclj1h51#

我想我发现了一些你可能会感兴趣的东西。
这篇文章:'Ruby 2.0 Works Hard So You Can Be Lazy' by Pat Shaughnessy解释了Eager和Lazy求值背后的思想,也解释了它如何与“框架类”(如Enumerale、Generator或Yielder)相关联。它主要集中在解释如何实现LazyEvaluation,但仍然相当详细。

原始来源:'Ruby 2.0 Works Hard So You Can Be Lazy' by Pat Shaughnessy

Ruby 2.0使用一个名为Enumerator::Lazy的对象实现了惰性求值。这个对象的特殊之处在于,它同时扮演了两个角色!它是一个枚举器,还包含了一系列的Enumerable方法。它调用每一个方法来从枚举源获取数据,并为枚举的其余部分生成数据。由于Enumerator::Lazy同时扮演了两个角色,因此可以将它们链接在一起以生成一个枚举。
这是Ruby中惰性求值的关键。来自数据源的每个值都被生成到我的块中,然后结果会立即沿着枚举链向下传递。这种枚举并不是即时的--Enumerator::Lazy#collect方法不会将值收集到数组中。相反,每个值都通过重复的生成沿着Enumerator::Lazy对象链一次传递一个。如果我将一系列对collect或其他Enumerator::Lazy方法的调用链接在一起,那么每个值都会沿着链从我的一个块传递到下一个块,一次一个
Enumerable#first通过在惰性枚举器上调用每个枚举器来启动迭代,并在迭代具有足够的值时通过引发异常来结束迭代。
归根结底,这是惰性评估背后的关键思想:位于计算链末端的函数或方法开始执行过程,程序的流程通过函数调用链反向工作,直到获得所需的数据输入。Ruby使用Enumerator::Lazy对象链来实现这一点。

fnvucqvd

fnvucqvd2#

Yielder只是一段代码,它返回值并等待下一次调用。
这可以通过使用Ruby Fiber Class轻松实现。请参见以下创建SimpleEnumerator类的示例:

class SimpleEnumerator

  def initialize &block
    # creates a new Fiber to be used as an Yielder
    @yielder  = Fiber.new do
      yield Fiber # call the block code. The same as: block.call Fiber
      raise StopIteration # raise an error if there is no more calls
    end
  end

  def next
    # return the value and wait until the next call
    @yielder.resume
  end

end

triangular_numbers = SimpleEnumerator.new do |yielder|
  number  = 0
  count   = 1
  loop do
    number  += count
    count   += 1
    yielder.yield number
  end
end

print triangular_numbers.next, " " 
print triangular_numbers.next, " " 
print triangular_numbers.next, " "

我只是用SimpleEnumerator.new替换了代码中的Enumerator.new,结果是一样的。
存在“轻量级协作并发”;使用Ruby文档中的词语,程序员在其中安排应该做什么,换句话说,程序员可以暂停和恢复代码块。

ut6juiuv

ut6juiuv3#

假设我们要打印前三个三角形数字,一个简单的实现是使用一个函数:

def print_triangular_numbers steps
  number = 0
  count = 1
  steps.times do
    number += count
    count += 1
    print number, " "
  end
end

print_triangular_numbers(3)

这里的缺点是我们将打印逻辑和计数逻辑混合在一起。如果我们不想打印数字,这就没有用。我们可以通过将数字 * yield * 到一个块中来改进这一点:

def triangular_numbers steps
  number = 0
  count = 1
  steps.times do
    number += count
    count += 1
    yield number
  end
end

triangular_numbers(3) { |n| print n, " " }

现在,假设我们要打印几个三角形数字,做一些其他的事情,然后继续打印它们。

def triangular_numbers steps, start = 0
  number = 0
  count = 1
  (steps + start).times do
    number += count
    yield number if count > start
    count += 1
  end
end

triangular_numbers(4) { |n| print n, " " }

# do other stuff

triangular_numbers(3, 4) { |n| print n, " " }

这样做的缺点是,每次我们想重新打印三角形数字时,都需要从头开始。效率很低!我们需要的是一种方法,它可以记住我们停止的地方,以便以后可以继续。带proc的变量提供了一个简单的解决方案:

number = 0
count = 1
triangular_numbers = proc do |&blk|
  number += count
  count += 1
  blk.call number
end

4.times { triangular_numbers.call { |n| print n, " " } }

# do other stuff

3.times { triangular_numbers.call { |n| print n, " " } }

但这是前进了一步,后退了两步。我们可以很容易地继续,但没有封装逻辑(我们可能会意外地更改number并破坏一切!)。我们 * 真正 * 想要的是一个对象,我们可以在其中存储状态。这正是Enumerator的用途。

triangular_numbers = Enumerator.new do |yielder|
  number = 0
  count = 1
  loop do
    number += count
    count += 1
    yielder.yield number
  end
end

4.times { print triangular_numbers.next, " " }

# do other stuff

3.times { print triangular_numbers.next, " " }

因为在Ruby中块是闭包,所以loop在调用之间记住numbercount的状态,这使得枚举器看起来像是并行运行的。
现在我们来看看yielder。注意,它取代了前面例子中使用proc的blk.call numberblk.call可以工作,但是不够灵活。在Ruby中,你不一定要提供块枚举器。有时候你只想一次枚举一个步骤,或者把枚举器链接在一起。Enumerator通过提供不可知的Enumerator::Yielder接口使枚举器的编写更加简单。当你给予yielder一个值时,(yielder.yield numberyielder << number),你告诉枚举器“无论何时有人要求下一个值(无论是块中的值、nexteach的值,还是直接传递给另一个枚举器的值),请给予他们这个值”。

dwthyt8l

dwthyt8l4#

我在Ruby Cookbook中找到了一个简洁的答案:
https://books.google.com/books?id=xBmkBwAAQBAJ&pg=PT463&lpg=PT463&dq=upgrade+ruby+1.8+generator&source=bl&ots=yyVBoNUhNj&sig=iYXXR_8QqVMasFnS53sbUzGAbTc&hl=en&sa=X&ei=fOM-VZb0BoXSsAWulIGIAw&ved=0CFcQ6AEwBw#v=onepage&q=upgrade%20ruby%201.8%20generator&f=false
这显示了如何使用Ruby 2.0+ Enumerator类创建Ruby 1.8样式Generator

my_array = ['v1', 'v2']

my_generator = Enumerator.new do |yielder|
    index = 0
    loop do
        yielder.yield(my_array[index])
        index += 1
    end
end

my_generator.next    # => 'v1'
my_generator.next    # => 'v2'
my_generator.next    # => nil
iezvtpos

iezvtpos5#

Ruby 2.7引入了Enumerator#produce,用于从任意块创建一个无限枚举器,这导致了一种非常优雅、非常实用的方式来实现最初的问题:

irb(main):001:0> triangular_numbers = Enumerator.produce([1, 2]) { |(number, count)| [number + count, count + 1] }
=> #<Enumerator: #<Enumerator::Producer:0x00007f7c69868bd8>:each>
irb(main):002:0> triangular_numbers.first(13).map(&:first)
=> [1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91]
irb(main):003:0> _

相关问题