【问题标题】:How to stop iteration in a Enumerator::Lazy method?如何在 Enumerator::Lazy 方法中停止迭代?
【发布时间】:2014-01-12 03:54:33
【问题描述】:

我正在尝试为 Ruby 2 的 Enumerator::Lazy 类实现 take_until 方法。它应该类似于take_while 工作,但是当产生的块返回真时停止迭代。结果应该包括产生的块匹配的项目。

我的问题是如何发出迭代结束的信号?使用常规枚举器时,您可以在 each 方法中引发 StopIteration 错误以指示迭代器的结束。但这似乎不适用于惰性枚举:

class Enumerator::Lazy  
  def take_until
    Lazy.new(self) do |yielder, *values|
      yielder << values
      raise StopIteration if yield *values
    end
  end
end

(1..Float::INFINITY).lazy.take_until{ |i| i == 5 }.force

我也试图突破障碍,但没有效果。 The documentation for Enumerator::Lazy 似乎也没有帮助。

为什么使用take_while 不是一个有效的选项。

take_while 的主要问题在于,它的本质是会尝试评估多于您需要的一项。在我的应用程序中,枚举器不会产生数字,而是通过网络获取的消息。试图评估不存在的消息(还没有?)是一种非常不可取的阻止操作。以下人为设计的示例说明了这一点:

enum = Enumerator.new do |y|
  5.times do |i|
    y << i
  end
  sleep
end

enum.lazy.take_while{ |i| i < 5 }.force

要接收来自该枚举器的前五个项目,您需要评估第六个结果。这并不像它可能的那么懒惰。在我的用例中,这是不可取的,因为进程会阻塞。

为 Enumerator::Lazy 提供 take 的纯 Ruby 实现

标准库包含一个take 方法,它的功能与我想要的类似。它不使用块作为条件,而是使用数字,但是一旦达到该数字,它就会中断迭代,而不是再评估一个项目。继续上例:

enum.lazy.take(5).force

这不会到达第 6 项,因此不会阻塞。问题是标准库中的版本是用 C 实现的,我似乎无法弄清楚如何在纯 Ruby 中实现它。该方法的 ruby​​ 实现将是一个可以接受的答案。

提前致谢!

【问题讨论】:

  • 我不想问,但你不能只使用 take_while 并根据需要修改条件吗?
  • 这是一个有效的问题,但我认为答案是:不,我不能。我的用例涉及一个响应流,我特别想在遇到特定条件时对序列进行定界。 Take_while 不会包含匹配项本身,而是将序列返回到第一个“未命中”。希望这是有道理的。
  • 所以take_until 将是一个否定的take_while,它会增加一个yield next
  • @steenslag 看起来像。
  • 另一个注意事项:如果有人可以提供 taketake_while 的 Ruby 实现,我想我可以从中得出我的答案。

标签: ruby lazy-evaluation enumerator


【解决方案1】:

这是一个老问题,但无论如何:正如你所说,你真正需要的是Lazy#take_until,当然Lazy#take_while需要获得下一个项目来决定是否打破。我一直无法使用Lazy#new { ... } 实现Lazy#take_until,显然没有中断机制。这是一种可能的解决方法:

class Enumerator::Lazy  
  def take_until
    Enumerator.new do |yielder|
      each do |value|
        yielder << value
        break if yield(value)
      end
    end.lazy
  end
end

【讨论】:

    【解决方案2】:

    根据我的评论,我认为修改 take_while 是更好的选择(或者至少是一个有效的选择):

    (1..Float::INFINITY).lazy.take_while { |i| i < 6 }.force
    => [1, 2, 3, 4, 5]
    

    对于不太容易重写的更复杂的条件,添加一个变量:

    found = false
    (1..Float::INFINITY).lazy.take_while do |i|
      if i == 5
        found = true
      else
        !found
      end
    end.force
    => [1, 2, 3, 4, 5]
    

    您也可以根据最后一个块定义take_while

    class Enumerator::Lazy
      def take_until
        take_while do |*args|
          if !@found
            @found = yield(*args)
            true
          else
            false
          end
        end
      end
    end
    

    请注意,它也不会不必要地调用块:

    p (1..20).lazy.take_until{|i| p i; i == 5}.force
    p (1..20).lazy.take_until{|i| p i; i == 3}.force
    p (1..20).lazy.take_until{|i| p i; i == 8}.force
    

    【讨论】:

    • take_while 的主要问题在于,它本质上会尝试评估多于您需要的一项。在我的应用程序中,枚举器不会产生数字,而是通过网络获取的消息。尝试评估不存在的消息(还没有?)是一种非常不可取的阻止操作。这就是为什么 take_while 在我的场景中不是一个有效的选项。
    • 好吧,因此建议相应地重写条件。 :-| (但请稍后查看我编辑的答案。)
    • 那里... take_until 实现使用 take_while 发布,根据我最初的回答。还是我误解了您的编辑?
    • 它仍然会产生比必要的多一项。我认为使用take_while 时没有任何办法。例如,使用我上面编辑中的枚举评估 enum.lazy.take_until{ |i| i == 4 }.force 将阻塞。感谢您为此付出的所有努力。
    • 呃……等等。我希望enum.lazy.take_until{ |i| i == 4 }.force 产生 4 然后立即停止,个人。您希望它停在 3 处而不用 i 评估块,这样 i == 4?如果是这样,那怎么可能? :-)
    【解决方案3】:

    我刚刚找到了这个实现。这不是最优的,因为它会通过内部缓存结果来隐式地强制迭代。

    class Enumerator::Lazy
      def take_until
        if block_given?
          ary = []
          while n = self.next
            ary << n
            if (yield n) == true
              break
            end
          end
          return ary.lazy
        else
          return self
        end
      end
    end
    

    使用我的问题中的示例:

    enum = Enumerator.new do |y|
      5.times do |i|
        y << i
      end
      sleep
    end
    
    p enum.lazy.take_until{ |i| i == 4 }.force
    

    现在将返回[0, 1, 2, 3, 4]

    我将这个问题留待更长时间,看看是否有人提出了一个真正惰性的实现,但我怀疑我们会找到一个。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-18
      • 1970-01-01
      • 2015-12-15
      • 2011-09-11
      • 1970-01-01
      • 2012-08-26
      相关资源
      最近更新 更多