-->

What's the best way to return an Enumerator::L

2019-02-14 05:12发布

问题:

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?

回答1:

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.