如何在Enumerator :: Lazy方法中停止迭代?

时间:2013-12-23 22:05:08

标签: ruby lazy-evaluation enumerator

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

我的问题是我如何表示已达到迭代结束?使用常规枚举器时,可以在每个方法中引发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实现将是一个可接受的答案。

提前致谢!

3 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:0)

根据我的评论,修改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

答案 2 :(得分:0)

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

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]

我保持这个问题的开放时间有点长,看看是否有人想出一个真正懒惰的实现,但我怀疑我们会找到一个。