【问题标题】:Enumerator as an infinite generator in Ruby枚举器作为 Ruby 中的无限生成器
【发布时间】:2015-05-03 19:37:23
【问题描述】:

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

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

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

谁能解释一下这一切在编译器眼中是如何执行的?

【问题讨论】:

  • +1,好问题。每个人都知道,当给定一个块时,枚举器可以将枚举一个一个地交给该块。基本调查会告诉你,一个新建的枚举器会为自己创建一个Enumerator::Yielder 的实例。但是,Enumerator::Yielderis empty 的文档。很难说是什么原因导致设计师将让步的任务(还有什么?)从Enumerator 委托给Enumerator::Yielder。我期待着答案。
  • @daremarkovic:IIRC,实际上,它没有并行执行。下面的机制称为“协程”,它“只是”回调/委托的智能编排。
  • @BorisStitnicky:请参阅我找到的文章。我发现它很有趣,我认为关于“yielder”的部分解释了您的疑惑。
  • @quetzalcoatl:那是一篇很棒的文章。很好的找到它。但是,在你的下辈子,请选择一个更容易输入的名字 =)
  • @BorisStitnicky:谢谢!仅供参考:我最近发现 Stack 网站上有一些 TAB 补全 :) 当您开始用@然后字母写评论时,会出现一个带有提示的工具提示,然后 TAB 补全目标用户名自动地。不幸的是,它似乎只有在 @ 是评论的第一个字符时才有效。

标签: ruby


【解决方案1】:

我想我发现了一些你可能会感兴趣的东西。

这篇文章:'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 都通过在惰性枚举器上调用 each 来开始迭代,并通过在具有足够值时引发异常来结束迭代。

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

【讨论】:

    【解决方案2】:

    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, " " 
    

    我刚刚将您代码中的Enumerator.new 替换为SimpleEnumerator.new,结果是一样的。

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

    【讨论】:

    • 惊人的答案。这个答案非常有价值,因为它甚至在原始发帖人意识到 ze 必须问这些问题之前就回答了这些问题。谢谢你教我Fiber
    • 很酷的实现,但是作为一个了解yielders而不了解Fibers的人,这更加令人困惑。
    • @Max 关于 Fibers 的 Ruby 文档非常清晰易懂;这就是为什么我包含链接而不试图详细解释它的原因。上面的实现试图说明 Yielder 使用 Ruby 语言在幕后做了什么。
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      我在 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
      

      【讨论】:

      • yield.yield(index) 怎么样。然后你的例子就变成了真正的无限。
      • 如果你想要无限的收益,那就行了。就我而言,我只想遍历数组中的项目。
      • 你可以在irb中测试@Lewisou的想法:my_array = ['v1', 'v2'];my_generator = Enumerator.new do |yielder|; index = 0; loop do; yielder.yield(my_array[index]); index += 1; index = 0 if index == 2; end; end;my_generator.next; # => 'v1'my_generator.next; # => 'v2'my_generator.next; # => 'v1'
      猜你喜欢
      • 1970-01-01
      • 2017-03-02
      • 2012-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-28
      • 2021-06-02
      相关资源
      最近更新 更多