Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stop iteration in a Enumerator::Lazy method?

I am trying to implement a take_until method for Ruby 2's Enumerator::Lazy class. It should work similar to take_while but instead stop iteration when the yielded block returns true. The result should include the item where the yielded block matches.

My question is how do I signal that the end of the iteration is reached? When using regular Enumerators you can raise the StopIteration error in an each method to signal the end of the iterator. But that doesn't seem to work for lazy enum's:

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

I also tried to break out of the block to no effect. The documentation for Enumerator::Lazy doesn't seem to help either.

Why using take_while is not a valid option.

The main problem with take_while is that by its nature it will attempt to evaluate one more item than you need. In my application the Enumerator doesn't yield numbers, but messages fetched over the network. Trying to evaluate a message that is not there (yet?) is a blocking action which is highly undesirable. This is illustrated by the following contrived example:

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

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

To receive the first five items from this enumerator you will need to evaluate the sixth result. This is not as lazy as it could be. In my use case this is undesirable since the process would block.

Providing a pure Ruby implementation of take for Enumerator::Lazy

The standard library includes a take method that does something similar to what I want. It doesn't use a block as a condition but a number, but it does break out of the the iteration once that number is reached instead of evaluating one more item. Following on from the example above:

enum.lazy.take(5).force

This does not get to the 6th item and so does not block. Problem is the version in the standard library is implemented in C and I can't seem to figure out how this could be implemented in pure Ruby. A ruby implementation of that method would be an acceptable answer.

Thanks in advance!

like image 351
Dirk Geurs Avatar asked Dec 23 '13 22:12

Dirk Geurs


People also ask

What does the lazy method do to enumerators?

Enumerator::Lazy. Enumerator::Lazy is a special type of Enumerator , that allows constructing chains of operations without evaluating them immediately, and evaluating values on as-needed basis. In order to do so it redefines most of Enumerable methods so that they just construct another lazy enumerator.

What is lazy evaluation in Ruby?

Lazy enumeration means that an expression is only processed when we are working with the result. There are languages that are lazy by default, like Haskell, and many others are implementing a way to lazily evaluate expressions.

What is an enumerator in Ruby?

Enumerator, specifically, is a class in Ruby that allows both types of iterations – external and internal. Internal iteration refers to the form of iteration which is controlled by the class in question, while external iteration means that the environment or the client controls the way iteration is performed.


1 Answers

It's an old question, but anyway: as you say, what you really need is a Lazy#take_until, of course Lazy#take_while will need to obtain the next item to decide whether to break or not. I've been unable to implement Lazy#take_until using Lazy#new { ... }, apparently there is no breaking mechanism. That's a possible workaround:

class Enumerator::Lazy  
  def take_until
    Enumerator.new do |yielder|
      each do |value|
        yielder << value
        break if yield(value)
      end
    end.lazy
  end
end
like image 128
tokland Avatar answered Nov 15 '22 04:11

tokland