What's the best way to return an Enumerator::Lazy when your class doesn't define #each?
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 aneach
, not justlazy
.You can define an
each
method that requires a parameter if you like and includeEnumerable
. Most methods ofEnumerable
won't work, buteach_with_index
and a couple of others will forward arguments so these would be usable immediately.The
Enumerator.new
without a block is gone becauseto_enum
is 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_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 onEnumerable
that callsto_enum
will keep a lazy enumerator lazy.
Returning Enumerator in own each method
Your first issue is that you're not explicitly returning anything from your method. In the case that a block is passed, the convention for each
is to return self
, as Array
does.
In the case that a block is not given, the standard way to create an Enumerator
is with the Kernel#to_enum
method. So, for example, you can use the following as the first line of your method, as recommended in What's the best way to return an Enumerator::Lazy when your class doesn't define #each?
return to_enum(:each) unless block_given?
Enumerator returning each line of a file, but chomped
You can create the enumerator as follows. First, let's create a file for demonstration.
str =<<_
Now is the
time for all
Rubiests to
come to the
aid of their
bowling team.
_
fname = "temp"
File.write(fname, str)
#=> 75
IO#foreach without a block returns an enumerator:
efe = File.foreach(fname)
#=> #<Enumerator: File:foreach("temp")>
so we need to merely embed this enumerator in another that chomps the newline character from a line:
echomp = Enumerator.new do |y|
loop do
y << efe.next.chomp
end
end
#=> #<Enumerator: #<Enumerator::Generator:0x007fbe128837b8>:each>
Let's try it:
echomp.next
#=> "Now is the"
echomp.next
#=> "time for all"
echomp.next
#=> "Rubiests to"
echomp.next
#=> "come to the"
echomp.next
#=> "aid of their"
echomp.next
#=> "bowling team."
echomp.next
#=> StopIteration: iteration reached an end
You could of course wrap this in a method:
def foreach_with_chomp(fname)
efe = File.foreach(fname)
Enumerator.new do |y|
loop do
y << efe.next.chomp
end
end
end
foreach_with_chomp(fname).each { |s| print "#{s} " }
Now is the time for all Rubiests to come to the aid of their bowling team.
Why return an enumerator?
This completely in accordance with the spirit of 1.9: to return enumerators whenever possible. String#bytes, String#lines, String#codepoints, but also methods like Array#permutation all return an enumerator.
In ruby 1.8 String#to_a resulted in an array of lines, but the method is gone in 1.9.
How to use an enumerator
The main distinction between an Enumerator
and most† other data structures in the Ruby core library (Array
, Hash
) and standard library (Set
, SortedSet
) is that an Enumerator
can be infinite. You cannot have an Array
of all even numbers or a stream of zeroes or all prime numbers, but you can definitely have such an Enumerator
:
evens = Enumerator.new do |y|
i = -2
y << i += 2 while true
end
evens.take(10)
# => [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
zeroes = [0].cycle
zeroes.take(10)
# => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
So, what can you do with such an Enumerator
? Well, three things, basically.
Enumerator
mixes inEnumerable
. Therefore, you can use allEnumerable
methods such asmap
,inject
,all?
,any?
,none?
,select
,reject
and so forth. Just be aware that anEnumerator
may be infinite whereasmap
returns anArray
, so trying tomap
an infiniteEnumerator
may create an infinitely largeArray
and take an infinite amount of time.There are wrapping methods which somehow "enrich" an
Enumerator
and return a newEnumerator
. For example,Enumerator#with_index
adds a "loop counter" to the block andEnumerator#with_object
adds a memo object.You can use an
Enumerator
just like you would use it in other languages for external iteration by using theEnumerator#next
method which will give you either the next value (and move theEnumerator
forward) orraise
aStopIteration
exception if theEnumerator
is finite and you have reached the end.
† Eg., an infinite range: (1..1.0/0)
Is there a way to pass a size to `enum_for`/`to_enum`?
You can pass a block to enum_for
that calculates the enum's size:
enum_for(:each) { width * height }
Get slice of an Enumerator effectively
An Enumerator
is very general interface, it makes only very simple assumptions about the "collection" it is traversing. In particular, it really only supports two operations: get the current element and iterate to the next element.
Given those two operations, if you want to get the 10 millionth element, there is only one thing you can do: iterate 10 million times. Which takes time.
There is no such thing as "slicing" an Enumerator
. An Enumerator
enumerates. That's it.
Now, as you discovered, there is another problem: Ruby's collection operations are not type-preserving. No matter what type of collection you call map
or select
or take
or whatever on, it will always return the same type: a fully realized, concrete, strict Array
. That's how most collection frameworks in most languages work, e.g. in .NET all collection operations return IEnumerable
. That's because most of these methods have only a single common implementation in the Enumerable
mixin.
Smalltalk is an exception, but there is another problem: the collection operations are duplicated for every single collection type. Every collection type has its own nearly-indetical practically copy&paste implementation of collect:
, select:
etc. This code duplication is hard to maintain and places a big burden on anyone who wants to integrate their own collection into the framework. In Ruby, it's easy: implement each
, mixin Enumerable
and you're done.
Note: as of Ruby 1.9, there is actually some of that duplication: Hash
implements its own version of select
which does actually return a Hash
and not an Array
. So, now, not only is there code duplication but also an asymmetry in the interface: all implementations of select
return Array
s except for the one in Hash
.
The Scala 2.8 collection framework is the first time ever that someone has figured out how to provide type-preserving collection operations without code duplication. But Ruby's collection framework was designed 15 years before Scala 2.8, so it cannot take advantage of that knowledge.
In Ruby 2.0, there are lazy Enumerator
s, where all collection operations return another lazy Enumerator
. But that won't help you here: the only difference is that the lazy Enumerator
will delay the 10 million iterations until you actually print
the values. It still has to perform those 10 million iterations because there is simply no way to do otherwise.
If you want slicing, you need a sliceable data structure, such as an Array
.
Related Topics
Ssl_Connect Error When Accessing Shopify API with Rubygem
Rails 3 and PDFkit. How to Specify Page Size
The Program 'Rails' Is Currently Not Installed
Ruby: Too Many Open Files @ Rb_Sysopen
How to Use Headless Chrome with Capybara and Selenium
Ruby Strftime '%Z' Method Returns '0545' Instead of 'Npt'
Ruby Spreadsheet Row Background Color
Sending Array of Values to a SQL Query in Ruby
Google Analytics API Error "Selected Dimensions and Metrics Cannot Be Queried Together."
Unescaping Characters in a String with Ruby
Using Ruby and Mechanize to Fill in a Remote Login Form Mystery
How to Parse Xml Nodes to CSV with Ruby and Nokogiri
How to Get a Listing of Only Files Using Dir.Glob
Monitor Ruby Processes with Monit
Does It Matter If a Conditional Statement Comes Before or After the Expression