Project

General

Profile

Actions

Feature #10498

open

Make `loop` yield a counter

Added by cesario (Franck Verrot) over 9 years ago. Updated about 6 years ago.

Status:
Open
Target version:
-
[ruby-core:66220]

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

0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch (1.74 KB) 0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch cesario (Franck Verrot), 11/12/2014 06:25 AM
0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch (1.86 KB) 0001-vm_eval.c-loop-now-yields-a-incremented-counter.patch Updated patch (no more limited to FIXNUM_MAX iterations) cesario (Franck Verrot), 11/14/2014 10:27 PM

Updated by marcandre (Marc-Andre Lafortune) over 9 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) over 9 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) over 9 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) over 9 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) over 9 years ago

  • Description updated (diff)

Updated by cesario (Franck Verrot) over 9 years ago

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) over 9 years ago

I agree with Martin, this should be Kernel#loop_with_index

Updated by cesario (Franck Verrot) over 9 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) over 9 years ago

Franck Verrot wrote:

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?

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) over 9 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) over 9 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) over 9 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) over 9 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) over 9 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) over 9 years ago

I always thought it would be most convenient if all loops had an intrinsic counter $i.

Updated by rklemme (Robert Klemme) over 9 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) over 9 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 BigNums, 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) over 9 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 BigNums, which compared to the code you're having in the block would probably be more expensive than incrementing a BigNum.

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) over 9 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 BigNums, which compared to the code you're having in the block would probably be more expensive than incrementing a BigNum.

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) over 9 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) about 9 years ago

  • Assignee changed from core to matz (Yukihiro Matsumoto)
Actions #22

Updated by naruse (Yui NARUSE) about 6 years ago

  • Target version deleted (2.2.0)
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0