Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the best way to return an Enumerator::Lazy when your class doesn't define #each?

Enumerable#lazy relies on your enumerable providing an #each method. If your enumerable doesn't have an #each method you can't use #lazy. Now Kernel#enum_for and #to_enum provide the flexibility to specify an enumeration method other than #each:

Kernel#enum_for(method = :each, *args)

But #enum_for and friends always construct plain (non-lazy) enumerators, never Enumerator::Lazy.

I see that Enumerator in Ruby 1.9.3 offers this similar form of #new:

Enumerator#new(obj, method = :each, *args)

Unfortunately that constructor has been completely removed in Ruby 2.0. Also I don't think it was ever available at all on Enumerator::Lazy. So it seems to me that if I have a class with a method I want to return a lazy enumerator for, if that class has no #each then I have to define some helper class that does define #each.

For instance, I've got a Calendar class. It doesn't really make sense for me to offer to enumerate every date from the beginning of all time. An #each would be useless. Instead I offer a method that enumerates (lazily) from a starting date:

  class Calendar
    ...
    def each_from(first)
      if block_given?
        loop do
          yield first if include?(first)
          first += step
        end
      else
        EachFrom.new(self, first).lazy
      end
    end
  end

And that EachFrom class looks like this:

class EachFrom
  include Enumerable
  def initialize(cal, first)
    @cal   = cal
    @first = first
  end
  def each
    @cal.each_from(@first) do |yielder, *vals|
      yield yielder, *vals
    end
  end
end

It works but it feels heavy. Maybe I should subclass Enumerator::Lazy and define a constructor like that deprecated one from Enumerator. What do you think?

like image 663
Bill Burcham Avatar asked Apr 12 '13 01:04

Bill Burcham


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.

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.


Video Answer


1 Answers

I think you should return a normal Enumerator using to_enum:

class Calendar
  # ...
  def each_from(first)
    return to_enum(:each_from, first) unless block_given?
    loop do
      yield first if include?(first)
      first += step
    end
  end
end

This is what most rubyists would expect. Even though it's an infinite Enumerable, it is still usable, for example:

Calendar.new.each_from(1.year.from_now).first(10) # => [...first ten dates...]

If they actually need a lazy enumerator, they can call lazy themselves:

Calendar.new.each_from(1.year.from_now)
  .lazy
  .map{...}
  .take_while{...}

If you really want to return a lazy enumerator, you can call lazy from you method:

  # ...
  def each_from(first)
    return to_enum(:each_from, first).lazy unless block_given?
    #...

I would not recommend it though, since it would be unexpected (IMO), could be an overkill and will be less performant.

Finally, there are a couple of misconceptions in your question:

  • All methods of Enumerable assume an each, not just lazy.

  • You can define an each method that requires a parameter if you like and include Enumerable. Most methods of Enumerable won't work, but each_with_index and a couple of others will forward arguments so these would be usable immediately.

  • The Enumerator.new without a block is gone because to_enum is what one should use. Note that the block form remains. There's also a constructor for Lazy, but it's meant to start from an existing Enumerable.

  • You state that to_enum never creates a lazy enumerator, but that's not entirely true. Enumerator::Lazy#to_enum is specialized to return a lazy enumerator. Any user method on Enumerable that calls to_enum will keep a lazy enumerator lazy.

like image 192
Marc-André Lafortune Avatar answered Nov 10 '22 16:11

Marc-André Lafortune