Enumerator#each return value is surprising

  • Thread starter Christopher Dicely
  • Start date
C

Christopher Dicely

Generally in Ruby, if enum is an Enumerable, enum.each returns enum --
this seems to be the case for all enums. But for Enumerator, it seems
to return...well, something else. For enumerators created with
enum_for, it returns the return value of the underlying function,
e.g.:

irb(main):065:0> e = [1,2,3].enum_for:)each)
=> #<Enumerator: [1, 2, 3]:each>
irb(main):066:0> e.each {}
=> [1, 2, 3]

While the return value of each is usually not that important (its
usually used as if it was a procedure rather than for its return
value), shouldn't Enumerator#each return the receiver like Array#each,
Hash#each, etc.?
 
D

Dhruva Sagar

[Note: parts of this message were removed to make it a legal post.]

Hi,

ruby-1.9.1-p378 > e = [1,2,3].enum_for:)each)
=> #<Enumerator:0x00000001577ce8>
ruby-1.9.1-p378 > e.each {}
=> [1, 2, 3]
ruby-1.9.1-p378 > e.each {|i| p i}
1
2
3
=> [1, 2, 3]

I think it's working just as one would expect it to...what you see as the
return value at the end is 'e' itself.
 
R

Ryan Davis

Hi,
=20
ruby-1.9.1-p378 > e =3D [1,2,3].enum_for:)each)
=3D> #<Enumerator:0x00000001577ce8>
ruby-1.9.1-p378 > e.each {}
=3D> [1, 2, 3]
ruby-1.9.1-p378 > e.each {|i| p i}
1
2
3
=3D> [1, 2, 3]
=20
I think it's working just as one would expect it to...what you see as = the
return value at the end is 'e' itself.

No, it isn't. 'e' itself was output on the second line of your irb =
session. What each is returning is what 'e' is wrapping up, the array.

That said, I think the result is as it should be, as enumerators should =
be (in my mind at least) acting as stand-ins for the real thing, they =
should be substitutable with the real thing and wind up with the same =
result:
a =3D [1, 2, 3] =3D> [1, 2, 3]
a.each {} =3D> [1, 2, 3]
e =3D a.enum_for:)each)
=3D> # said:
e.each {}
=3D> [1, 2, 3]

I think that is a nice amount of symmetry.=
 
C

Christopher Dicely

That said, I think the result is as it should be, as enumerators should be (in my mind at least) acting as stand-ins for
the real thing, they should be substitutable with the real thing and wind up with the same result:

I suppose when the enumerator is created with the default options, I
can sort of see that (or at least, see it as unlikely to be harmful.)
When its created with enum_for with an argument other than :each,
though, it seems to me like you end up with more possibility for
unexpected results.

e.g.,

array = ["a","b","c"]
enum = array.enum_for:)each_with_index)
enum.each {} # => ["a","b","c"]
enum.to_a.each {} # => [["a", 0], ["b", 1], ["c", 2]]

That is, even ignoring the implementation class, the sequence returned
by Enumerator#each isn't always the same sequence that the Enumerator
represents. This isn't like any other Enumerable in core Ruby.
 
Y

Yossef Mendelssohn

I suppose when the enumerator is created with the default options, I
can sort of see that (or at least, see it as unlikely to be harmful.)
When its created with enum_for with an argument other than :each,
though, it seems to me like you end up with more possibility for
unexpected results.

e.g.,

array =3D ["a","b","c"]
enum =3D array.enum_for:)each_with_index)
enum.each {} # =3D> ["a","b","c"]
enum.to_a.each {} # =3D> [["a", 0], ["b", 1], ["c", 2]]

That is, even ignoring the implementation class, the sequence returned
by Enumerator#each isn't always the same sequence that the Enumerator
represents. This isn't like any other Enumerable in core Ruby.

First system ruby 1.8.6, then rvm with 1.9.1

$ irb
%w[a b c].each {} =3D> ["a", "b", "c"]
%w[a b c].each_with_index {} =3D> ["a", "b", "c"]
exit

$ rvm use ruby-1.9.1
$ irb
%w[a b c].each
=3D> # said:
_.each {}
=3D> ["a", "b", "c"]
%w[a b c].each {} =3D> ["a", "b", "c"]
%w[a b c].each_with_index {} =3D> ["a", "b", "c"]
%w[a b c].each_with_index
=3D> # said:
_.each {}
=3D> ["a", "b", "c"]

You should learn to expect the original array as a return value. It's
not about what kind of `each` enumeration you use, but the ability to
chain calls as expected. If you want to get something different, use a
transforming enumerator instead of `each`.
 
C

Christopher Dicely

You should learn to expect the original array as a return value. It's
not about what kind of `each` enumeration you use, but the ability to
chain calls as expected.

I think that is exactly what gets broken, though: Enumerator is the
only core Ruby class that mixes in Enumerable on which chained calls
to #each do not each get sent to the same receiver. To me, this seems
to be a leaky abstraction that adds a subtle potential complication to
code that operates on Enumerables.

If you want to get something different, use a
transforming enumerator instead of `each`.

I understand HOW the existing operations work. What I'm trying to
understand is the rationale, in part because I'm working on doing some
work on implementing some extensions to Enumerable to provide some a
few additional querying/joining operations over Ruby Enumerables
(vaguely like LINQ to Objects in .NET.) Naturally, this involves
implementing custom Enumerators. My inclination has always been that
custom Enumerables should behave like Ruby's core enumerables (except,
I now realize, Enumerator) and return the receiver from #each for
consistency. Since the core enumerators don't do that, though, I'd
like to avoid an implementation that is going to violate the
expectations of Enumerators.
 
D

David A. Black

Hi --

That said, I think the result is as it should be, as enumerators should be (in my mind at least) acting as stand-ins for
the real thing, they should be substitutable with the real thing and wind up with the same result:

I suppose when the enumerator is created with the default options, I
can sort of see that (or at least, see it as unlikely to be harmful.)
When its created with enum_for with an argument other than :each,
though, it seems to me like you end up with more possibility for
unexpected results.

e.g.,

array = ["a","b","c"]
enum = array.enum_for:)each_with_index)
enum.each {} # => ["a","b","c"]
enum.to_a.each {} # => [["a", 0], ["b", 1], ["c", 2]]

That is, even ignoring the implementation class, the sequence returned
by Enumerator#each isn't always the same sequence that the Enumerator
represents. This isn't like any other Enumerable in core Ruby.

I'd say that the enumerator represents enumeration logic (i.e., the
logic borrowed from its "host" method), rather than a particular
sequence. So you get the following parallels:

ruby-1.9.1-p378 > a = [1,2,3,4]
=> [1, 2, 3, 4]
ruby-1.9.1-p378 > e = a.each_with_index
=> #<Enumerator:0x0000000af4dcb0>

ruby-1.9.1-p378 > a.each_with_index.to_a
=> [[1, 0], [2, 1], [3, 2], [4, 3]]
ruby-1.9.1-p378 > e.to_a
=> [[1, 0], [2, 1], [3, 2], [4, 3]]

ruby-1.9.1-p378 > a.each_with_index.each
=> #<Enumerator:0x0000000af0eb28>
ruby-1.9.1-p378 > e.each
=> #<Enumerator:0x0000000af4dcb0> # returns itself, since it's
# already an enumerator
ruby-1.9.1-p378 > a.each_with_index.each {}
=> [1, 2, 3, 4]
ruby-1.9.1-p378 > e.each {}
=> [1, 2, 3, 4]

In other words, the enumerator is behaving like a.each_with_index, not
like a and not like itself.to_a. This isn't surprising, of course, since
e is the return value from a.each_with_index :) And I think it's
consistent. In particular, note the last pair of examples, which both
return the original array object.

I guess I'm not sure how else it could work, since anything else would
(I think) be a departure from the idea that the enumerator gets its each
logic not just from a particular object but from an object/method
combination.


David

--
David A. Black, Senior Developer, Cyrus Innovation Inc.

The Ruby training with Black/Brown/McAnally
Compleat Philadelphia, PA, October 1-2, 2010
Rubyist http://www.compleatrubyist.com
 
C

Christopher Dicely

I guess I'm not sure how else it could work, since anything else would
(I think) be a departure from the idea that the enumerator gets its each
logic not just from a particular object but from an object/method
combination.

I always really thought of Enumerators as representing a sequence with
the logic that produces the sequence being an implementation detail,
but I see that perspective. More importantly, I see the utility of
returning the underlying object in terms of making chained calls using
intermediate methods that produce an enumerator work like regular
iterator chaining.

I'll need to think a bit about how that generalizes to some of the
custom enumerators I'm doing.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,902
Latest member
Elena68X5

Latest Threads

Top