问题
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
Enumerableassume aneach, not justlazy.You can define an
eachmethod that requires a parameter if you like and includeEnumerable. Most methods ofEnumerablewon't work, buteach_with_indexand a couple of others will forward arguments so these would be usable immediately.The
Enumerator.newwithout a block is gone becauseto_enumis what one should use. Note that the block form remains. There's also a constructor forLazy, but it's meant to start from an existingEnumerable.You state that
to_enumnever creates a lazy enumerator, but that's not entirely true.Enumerator::Lazy#to_enumis specialized to return a lazy enumerator. Any user method onEnumerablethat callsto_enumwill keep a lazy enumerator lazy.
来源:https://stackoverflow.com/questions/15961453/whats-the-best-way-to-return-an-enumeratorlazy-when-your-class-doesnt-define