Feature #10498
openMake `loop` yield a counter
Description
Problem¶
Teaching Ruby, we always end up with that type of construct
i = 0
loop do
i += 1
# do something with i....
raise StopIteration if i ...
end
Solution¶
What I propose with this patch is making loop
yield the iteration count:
loop do |i|
# do something with i....
raise StopIteration if i ...
end
i
starts at 0 and stops at FIXNUM_MAX
(there's no Float::Infinity
equivalent for integers).
Alternate solution¶
Integer#times
could work if we had an <Integer's infinity>
object, so we would just do <Integer's Infinity>.times { |i| ... }
.
Also, this is the very first patch I submit to Ruby, I might have done something horrible, feel free to tell me :-)
Files
Updated by marcandre (Marc-Andre Lafortune) about 10 years ago
I've also expected loop
to yield a number and forget from time to time it doesn't.
Note you can achieve the same effect today with (0..Float::INFINITY).each
. It's explicit, but quite a bit longer.
Updated by chrisseaton (Chris Seaton) about 10 years ago
But doesn't this mean #loop will only run FIXNUM_MAX times? Rather than run infinitely as it currently does? That's a pretty big semantic change. Also, why not just overflow to Bignum?
Updated by marcandre (Marc-Andre Lafortune) about 10 years ago
Chris Seaton wrote:
But doesn't this mean #loop will only run FIXNUM_MAX times?
Indeed, the patch is not acceptable.
Let's consider the feature request that loop infinitely and yields a number (that will eventually be a Bignum). If this is accepted, writing the patch won't be a big issue.
I forgot another existing alternative: loop.with_index do |_, i|
gets the same effect.
Updated by duerst (Martin Dürst) about 10 years ago
Shouldn't the version of loop that yields a number be called loop_with_index, to correspond with others such as each_with_index, map_with_index, and so forth? Maybe with a little bit of magic, that can be made to happen?
Updated by nobu (Nobuyoshi Nakada) about 10 years ago
- Description updated (diff)
Updated by cesario (Franck Verrot) about 10 years ago
- File 0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch 0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch added
Marc-Andre Lafortune wrote:
Chris Seaton wrote:
But doesn't this mean #loop will only run FIXNUM_MAX times?
Indeed, the patch is not acceptable.
This indeed was a mistake, I've re-submitted a new patch.
So now the counter starts at 0 and will eventually become a Bignum when bigger than FIXNUM_MAX.
Updated by phluid61 (Matthew Kerwin) about 10 years ago
I agree with Martin, this should be Kernel#loop_with_index
Updated by cesario (Franck Verrot) about 10 years ago
Martin Dürst wrote:
Shouldn't the version of loop that yields a number be called loop_with_index, to correspond with others such as each_with_index, map_with_index, and so forth? Maybe with a little bit of magic, that can be made to happen?
I might have overlooked them, but I can't find any reference to any other*_with_index
method than each_with_index
. Are they in Ruby core or in an external gem?
Updated by joffreyjaffeux (Joffrey Jaffeux) about 10 years ago
Franck Verrot wrote:
I might have overlooked them, but I can't find any reference to any other
*_with_index
method thaneach_with_index
. Are they in Ruby core or in an external gem?
each_with_index
does exist, but concerning map
, Martin was probably talking about map.with_index
as defined in http://ruby-doc.org/core-2.1.5/Enumerator.html#method-i-with_index
Using loop.with_index {|i| puts i}
will currently yield nil.
Updated by funny_falcon (Yura Sokolov) about 10 years ago
> loop.with_index.take(10)
=> [[nil, 0], [nil, 1], [nil, 2], [nil, 3], [nil, 4], [nil, 5], [nil, 6], [nil, 7], [nil, 8], [nil, 9]]
> loop.with_index{|_,i| p i; break}
0
=> nil
Updated by funny_falcon (Yura Sokolov) about 10 years ago
> LOOP = 2**1000
=> 10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376
> LOOP.times.take(10)
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
> LOOP.times{|i| p i; break if i == 2}
0
1
2
=> nil
But still, if loop
yields index it will not damage anyone.
Updated by recursive-madman (Recursive Madman) about 10 years ago
But still, if
loop
yields index it will not damage anyone.
If existing code passes a lambda (or anything else with argument checking), it would damage them.
I think having loop yield a counter is very useful, but it should check #arity != 0
for backwards compatibility.
Updated by funny_falcon (Yura Sokolov) about 10 years ago
I think having loop yield a counter is very useful, but it should check #arity != 0 for backwards compatibility.
You are right. +1
Updated by znz (Kazuhiro NISHIYAMA) about 10 years ago
How about Numeric#step
?
>> 0.step.take(3)
=> [0, 1, 2]
>> 1.step.take(3)
=> [1, 2, 3]
Updated by trans (Thomas Sawyer) about 10 years ago
I always thought it would be most convenient if all loops had an intrinsic counter $i
.
Updated by rklemme (Robert Klemme) about 10 years ago
I am actually against this feature. Reason: an infinite loop does not need a counter. We incur the cost of counting (especially when the figure leaves Fixnum space) on all infinite loops. For loops that have a fixed condition on the number (some have been shown with step take or other) that condition can be used upfront with a range or #step.
One can create a counting infinite loop like this:
x = Enumerator::Generator.new {|y| i = 0; loop {y << i; i += 1}}
or even
x = Enumerator::Generator.new {|y| i = -1; loop {y << (i += 1)}}
If needed that can be made a constant somewhere, e.g. in Enumerator.
Updated by cesario (Franck Verrot) about 10 years ago
Hi Robert, thanks for taking time reading the ticket.
Robert Klemme wrote:
I am actually against this feature. Reason: an infinite loop does not need a counter.
Most examples I can find about it (and use personally when teaching) make use of a counter (all languages). Keeping track of the current iteration can be useful.
We incur the cost of counting (especially when the figure leaves Fixnum space) on all infinite loops.
I definitely agree . I wasn't able to find a way to fine-tune this based on the block's arity as mentioned previously.
On the other hand, you'd need 4,611,686,018,427,387,903 iterations before paying the price of using BigNum
s, which compared to the code you're having in the block would probably be more expensive than incrementing a BigNum
.
So I'm not sure whether or not that price should be considered high. Anything I'm missing?
x = Enumerator::Generator.new {|y| i = -1; loop {y << (i += 1)}}
If needed that can be made a constant somewhere, e.g. in Enumerator.
Given the context, I'm afraid I wouldn't use this unless it's a well-known constant.
Thanks again, looking forward to more insights :-)
Updated by rklemme (Robert Klemme) about 10 years ago
Franck Verrot wrote:
Hi Robert, thanks for taking time reading the ticket.
You're welcome!
Robert Klemme wrote:
I am actually against this feature. Reason: an infinite loop does not need a counter.
Most examples I can find about it (and use personally when teaching) make use of a counter (all languages). Keeping track of the current iteration can be useful.
"can"!
We incur the cost of counting (especially when the figure leaves Fixnum space) on all infinite loops.
I definitely agree . I wasn't able to find a way to fine-tune this based on the block's arity as mentioned previously.
That seems fairly easy:
def lp(&b)
return to_enum(:lp) unless b
if b.arity == 0
while true
b[]
end
else
i = 0
while true
b[i]
i += 1
end
end
raise "This must never happen"
end
On the other hand, you'd need 4,611,686,018,427,387,903 iterations before paying the price of using
BigNum
s, which compared to the code you're having in the block would probably be more expensive than incrementing aBigNum
.
Yes, but as a user of loop you would not have a choice any more. I'd rather use the approach from above or define another method loop_with_index or use the approach with Generator and a constant. I would definitively not unconditionally provide a counter.
So I'm not sure whether or not that price should be considered high. Anything I'm missing?
I think it is generally bad to do unnecessary work. Also it is inelegant. :-)
Updated by cesario (Franck Verrot) about 10 years ago
Robert Klemme wrote:
[...] I wasn't able to find a way to fine-tune this based on the block's arity as mentioned previously.
That seems fairly easy:
def lp(&b) return to_enum(:lp) unless b if b.arity == 0 while true b[] end else i = 0 while true b[i] i += 1 end end raise "This must never happen" end
I meant in C but yes, given we could have Kernel#loop_with_index
implemented in Ruby, it would be a no-brainer.
On the other hand, you'd need 4,611,686,018,427,387,903 iterations before paying the price of using
BigNum
s, which compared to the code you're having in the block would probably be more expensive than incrementing aBigNum
.Yes, but as a user of loop you would not have a choice any more. I'd rather use the approach from above or define another method loop_with_index or use the approach with Generator and a constant. I would definitively not unconditionally provide a counter.
I get your point.
times
does yield a counter, would you say it's the same concern?
Updated by rklemme (Robert Klemme) about 10 years ago
Franck Verrot wrote:
Robert Klemme wrote:
Yes, but as a user of loop you would not have a choice any more. I'd rather use the approach from above or define another method loop_with_index or use the approach with Generator and a constant. I would definitively not unconditionally provide a counter.
I get your point.
times
does yield a counter, would you say it's the same concern?
No, because that is specifically designed for iterating a fixed number of times.
Updated by duerst (Martin Dürst) almost 10 years ago
- Assignee changed from core to matz (Yukihiro Matsumoto)
Updated by naruse (Yui NARUSE) almost 7 years ago
- Target version deleted (
2.2.0)
Updated by hsbt (Hiroshi SHIBATA) 8 months ago
- Status changed from Open to Assigned