24

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

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这里的目的,它需要什么价值,以及这段代码是如何与程序的其余代码并行执行的。

执行从顶部开始,并且可能在块“产生”我的代码的值时暂停。

有人可以解释一下这一切在编译器眼中是如何执行的吗?

4

4 回答 4

8

我想我发现了一些你可能会觉得有趣的东西。

这篇文章:Pat Shaughnessy 的“Ruby 2.0 努力工作,所以你可以懒惰”解释了 Eager 和 Lazy 评估背后的想法,并解释了它与 Enumerale、Generator 或 Yielder 等“框架类”的关系。它主要集中在解释如何实现 LazyEvaluation,但仍然非常详细。


原始来源:Pat Shaughnessy 的“Ruby 2.0 努力工作,所以你可以变得懒惰”

Ruby 2.0 使用名为 Enumerator::Lazy 的对象实现惰性求值。这个特别之处在于它扮演了两个角色!它是一个枚举器,也包含一系列的 Enumerable 方法。它调用每个从枚举源获取数据,并将数据生成给枚举的其余部分。由于 Enumerator::Lazy 扮演这两个角色,您可以将它们链接在一起以生成单个枚举。

这是 Ruby 中惰性求值的关键。来自数据源的每个值都会产生给我的块,然后结果会立即沿枚举链向下传递。这个枚举并不急切—— Enumerator::Lazy#collect 方法不会将值收集到数组中。相反,每个值一次通过 Enumerator::Lazy 对象链传递一个,通过重复产生。如果我将一系列对 collect 或其他 Enumerator::Lazy 方法的调用链接在一起,则每个值都将沿着链从我的一个块传递到下一个块,一次一个

Enumerable#first 都通过调用每个块开始迭代在惰性枚举器上,并通过在具有足够值时引发异常来结束迭代。

归根结底,这是惰性求值背后的关键思想:计算链末端的函数或方法开始执行过程,程序的流程通过函数调用链向后工作,直到它只获得数据它需要的输入。Ruby 使用 Enumerator::Lazy 对象链实现了这一点。

于 2013-10-29T13:56:45.617 回答
7

Yielder只是一段返回值并等待下一次调用的代码。

这可以通过使用RubyFiber轻松实现。请参阅以下创建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, " " 

Enumerator.new我只是在你的代码中替换了SimpleEnumerator.new,结果是一样的。

有“轻量级协同并发”;使用 Ruby 文档的话,程序员可以安排应该做什么,换句话说,程序员可以暂停和恢复代码块。

于 2013-10-29T13:59:45.247 回答
6

假设我们要打印前三个三角数。一个天真的实现是使用一个函数:

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

print_triangular_numbers(3)

这里的缺点是我们将打印逻辑与计数逻辑混合在一起。如果我们不想打印数字,这是没有用的。我们可以通过将数字生成一个块来改进这一点

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。请注意,它替换blk.call number了我们使用 proc 的上一个示例。blk.call工作,但它是不灵活的。在 Ruby 中,您不必总是为枚举器提供块。有时您只想一次枚举一个步骤或将枚举器链接在一起,在这些情况下让您的枚举器简单地将值传递给块是不方便的。通过提供不可知的接口Enumerator,使枚举器的编写更加简单。Enumerator::Yielder当你给 yielder (yielder.yield numberyielder << number) 一个值时,你是在告诉枚举器“每当有人要求下一个值时(无论是在一个块中,用next, each,还是直接传递给另一个枚举器),给他们这个。” yield关键字根本不会在这里剪掉它,因为它只是为块产生值。

于 2013-10-29T20:18:47.920 回答
1

我在 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-VZb0BoXSsAEwgradeBIGIAw#vQ&sa=X&ei=fOM-VZb0BoXSsAEwgradeBwcQ&6 20ruby%201.8%20generator&f=false

这显示了如何Generator使用 Ruby 2.0+Enumerator类创建 Ruby 1.8 样式。

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
于 2015-04-28T03:50:10.800 回答