483

I'm using this code to let the user enter in names while the program stores them in an array until they enter an empty string (they must press enter after each name):

people = []
info = 'a' # must fill variable with something, otherwise loop won't execute

while not info.empty?
    info = gets.chomp
    people += [Person.new(info)] if not info.empty?
end

This code would look much nicer in a do ... while loop:

people = []

do
    info = gets.chomp
    people += [Person.new(info)] if not info.empty?
while not info.empty?

In this code I don't have to assign info to some random string.

Unfortunately this type of loop doesn't seem to exist in Ruby. Can anybody suggest a better way of doing this?

Paige Ruten
  • 172,675
  • 36
  • 177
  • 197
  • 1
    I think the normal while loop looks nicer, and is easier to read. – Magne Nov 14 '13 at 10:57
  • 1
    @Jeremy Ruten is there any chance you would be interested in changing the accepted answer to Siwei Shen's answer, `loop do; ...; break if ...; end`? – David Winiecki May 03 '14 at 22:08

10 Answers10

701

CAUTION:

The begin <code> end while <condition> is rejected by Ruby's author Matz. Instead he suggests using Kernel#loop, e.g.

loop do 
  # some code here
  break if <condition>
end 

Here's an email exchange in 23 Nov 2005 where Matz states:

|> Don't use it please.  I'm regretting this feature, and I'd like to
|> remove it in the future if it's possible.
|
|I'm surprised.  What do you regret about it?

Because it's hard for users to tell

  begin <code> end while <cond>

works differently from

  <code> while <cond>

RosettaCode wiki has a similar story:

During November 2005, Yukihiro Matsumoto, the creator of Ruby, regretted this loop feature and suggested using Kernel#loop.

Jean
  • 825
  • 8
  • 17
Siwei
  • 19,858
  • 7
  • 75
  • 95
  • 21
    Spot on. This `begin end while` method didn't seem right. Thanks for giving me the fodder I needed to convince the rest of the team. – Joshua Pinter Nov 25 '12 at 22:39
  • 2
    It looks like the begin-end-while loop is actually evaluating the condition before running the loop. The difference between that and a regular while loop is that it's guaranteed to run at least once. It's just close enough to do...while to cause problems. – user1992284 Jun 12 '13 at 09:54
  • 4
    So,as far as I've understood well,begin-end-while is "regretted" because violates the semantic of modifiers, that is:they are checked before executing the block, for example: puts k if !k.nil?. Here 'if' is a 'modifier':it is checked BEFORE,in order to determine whether executing the 'puts k' statement.That's not the case of while/until loops that(when used as modifiers of a begin-end-block!),are evaluated AFTER the first execution.Maybe this caused the regretting,but have we something 'stronger' than an old forum post,sort of an official deprecation like it uses to be in many other occasions? – AgostinoX Jan 02 '15 at 17:40
  • This is the explanation I was looking for when I wondered why there is a Rubocop check for this (Lint/Loop) https://github.com/bbatsov/rubocop/blob/v0.34.2/lib/rubocop/cop/lint/loop.rb – Rob Oct 29 '15 at 19:04
  • 3
    It took me an embarrassing amount of time to figure out why my use of this ruby 'do-while' loop wasn't working. You should use 'unless' to more closely mimic a c-style do-while, otherwise you may end up like me and forget to invert the condition :p – Connor Clark Dec 17 '15 at 21:31
  • In my Ruby 2.2.3 & 1.9.3 while condition is evaluated after running begin-end loop. Check out this code snippet: https://repl.it/CDJE/0 – pablo Apr 07 '16 at 11:34
  • I don't understand, why would he add it to the language if he doesn't agree with its use? – TheBritishAreComing Oct 02 '18 at 11:21
  • 2
    @James As per the linked mail, he said he was "regretting" adding it. Sometimes people make mistakes, even if they're language designers. – Xiong Chiamiov Feb 20 '19 at 18:54
  • also preferred in Ruby style guide https://github.com/rubocop/ruby-style-guide#loop-with-break – equivalent8 Aug 23 '22 at 05:20
190

I found the following snippet while reading the source for Tempfile#initialize in the Ruby core library:

begin
  tmpname = File.join(tmpdir, make_tmpname(basename, n))
  lock = tmpname + '.lock'
  n += 1
end while @@cleanlist.include?(tmpname) or
  File.exist?(lock) or File.exist?(tmpname)

At first glance, I assumed the while modifier would be evaluated before the contents of begin...end, but that is not the case. Observe:

>> begin
?>   puts "do {} while ()" 
>> end while false
do {} while ()
=> nil

As you would expect, the loop will continue to execute while the modifier is true.

>> n = 3
=> 3
>> begin
?>   puts n
>>   n -= 1
>> end while n > 0
3
2
1
=> nil

While I would be happy to never see this idiom again, begin...end is quite powerful. The following is a common idiom to memoize a one-liner method with no params:

def expensive
  @expensive ||= 2 + 2
end

Here is an ugly, but quick way to memoize something more complex:

def expensive
  @expensive ||=
    begin
      n = 99
      buf = "" 
      begin
        buf << "#{n} bottles of beer on the wall\n" 
        # ...
        n -= 1
      end while n > 0
      buf << "no more bottles of beer" 
    end
end

Originally written by Jeremy Voorhis. The content has been copied here because it seems to have been taken down from the originating site. Copies can also be found in the Web Archive and at Ruby Buzz Forum. -Bill the Lizard

Community
  • 1
  • 1
hubbardr
  • 3,153
  • 1
  • 21
  • 27
  • 5
    Courtesy of the Wayback Machine: http://web.archive.org/web/20080206125158/http://archive.jvoorhis.com/articles/2007/06/13/ruby-hidden-do-while-loop – bta Jul 22 '10 at 15:32
  • 57
    This is why when linking to an external site, I always make sure to copy the relevant info into my answer here. – davr Sep 09 '10 at 00:03
  • 11
    Why would you expect the while modifier to be evaluated before the contents of begin...end? That's the way do..while loops are supposed to work. And why would you "be happy to never see this idiom again?" What's wrong with it? I'm confused. – bergie3000 Feb 13 '13 at 19:33
  • 2
    begin...end looks like a block, similarly {...}. Nothing wrong with it. – Victor Pudeyev Jun 08 '13 at 05:22
  • 1
    -1: Siwei Shen's answer explains that `begin .. end` is somewhat frowned upon. Use `loop do .. break if ` instead. – Kevin Oct 18 '13 at 19:27
  • @choonkeat use while then? i really dont understand what's wrong with common in other languages do while expression. – Fedcomp Jul 21 '15 at 08:10
  • 1
    @Fedcomp sorry, comparing with other languages is rather moot. the point is that ruby is inconsistent with itself, hence language designer regrets it. i've expanded my gist to clarify what "inconsistent" means https://gist.github.com/choonkeat/9815130 – choonkeat Jul 22 '15 at 03:32
  • Pretty sweet how you scored 176 upvotes for Jeremy Voorhis' answer! – Jacob Apr 01 '16 at 21:39
  • 1
    @Jacob I know :| It was originally just a link but as the commenters mention, it was taken down so an editor changed the post and copied in the content. This is not what I originally posted. :) – hubbardr Apr 01 '16 at 23:20
105

Like this:

people = []

begin
  info = gets.chomp
  people += [Person.new(info)] if not info.empty?
end while not info.empty?

Reference: Ruby's Hidden do {} while () Loop

Grant Winney
  • 65,241
  • 13
  • 115
  • 165
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
  • Won't this code add an empty string to the array if there is no input? – AndrewR Sep 25 '08 at 23:25
  • 1
    Although it does not apply here, one problem of the begin-end-while construct is that, unlike all other ruby constructions, it does not return the value of the last expression: "begin 1 end while false" returns nil (not 1, not false) – tokland Dec 23 '10 at 12:03
  • 9
    You can use `until info.empty?` rather than `while not info.empty?`. – Andrew Grimm Aug 02 '11 at 02:56
  • Actually @AndrewR that's sort of the point.. to do things before a compare.. do diplay("remaining =#{count} ) while(count > 0)... I get a display of "remaining = 0".. perfect! – baash05 Dec 16 '12 at 22:02
44

How about this?

people = []

until (info = gets.chomp).empty?
  people += [Person.new(info)]
end
AndrewR
  • 10,759
  • 10
  • 45
  • 56
  • 26
    But this isn't "do ... while" loop. :) – Alexander Prokofyev Sep 26 '08 at 04:50
  • 4
    But it does the same thing in this case, unless I'm mistaken – Blorgbeard Sep 30 '08 at 07:25
  • 21
    @Blorgbeard, a do..while loop always runs once, then evaluates to see if it should continue running. A traditional while/until loop can run 0 times. It isn't a HUGE difference, but they are different. – Scott Swezey Jan 07 '10 at 02:31
  • 5
    @Scott, that's true - I just meant that this code is equivalent to the OP's, even though it doesn't use a do/while. Although really, this code does half of the loop's "work" in the condition, so it's not quite a traditional while loop either - if the condition doesn't match, some work is still done. – Blorgbeard Apr 13 '10 at 21:13
  • Just tried this. You can have a block of the form begin ... end until. Cool! – Venkat D. Apr 18 '11 at 04:03
  • This is a while loop, not a do..while one. – davorb Aug 19 '11 at 03:22
  • Not a do while loop.. tis a simple while do. – baash05 Dec 16 '12 at 22:11
  • The question was whether someone could suggest a better way of writing the code, which does not necessarily require a do while. Since this answers the question elegantly, +1. – Eva Feb 14 '13 at 04:20
13

This works correctly now:

begin
    # statment
end until <condition>

But, it may be remove in the future, because the begin statement is counterintuitive. See: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/6745

Matz (Ruby’s Creator) recommended doing it this way:

loop do
    # ...
    break if <condition>
end
Steely Wing
  • 16,239
  • 8
  • 58
  • 54
12

Here's the full text article from hubbardr's dead link to my blog.

I found the following snippet while reading the source for Tempfile#initialize in the Ruby core library:

begin
  tmpname = File.join(tmpdir, make_tmpname(basename, n))
  lock = tmpname + '.lock'
  n += 1
end while @@cleanlist.include?(tmpname) or
  File.exist?(lock) or File.exist?(tmpname)

At first glance, I assumed the while modifier would be evaluated before the contents of begin...end, but that is not the case. Observe:

>> begin
?>   puts "do {} while ()" 
>> end while false
do {} while ()
=> nil

As you would expect, the loop will continue to execute while the modifier is true.

>> n = 3
=> 3
>> begin
?>   puts n
>>   n -= 1
>> end while n > 0
3
2
1
=> nil

While I would be happy to never see this idiom again, begin...end is quite powerful. The following is a common idiom to memoize a one-liner method with no params:

def expensive
  @expensive ||= 2 + 2
end

Here is an ugly, but quick way to memoize something more complex:

def expensive
  @expensive ||=
    begin
      n = 99
      buf = "" 
      begin
        buf << "#{n} bottles of beer on the wall\n" 
        # ...
        n -= 1
      end while n > 0
      buf << "no more bottles of beer" 
    end
end
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
jvoorhis
  • 191
  • 3
  • 4
6

From what I gather, Matz does not like the construct

begin
    <multiple_lines_of_code>
end while <cond>

because, it's semantics is different than

<single_line_of_code> while <cond>

in that the first construct executes the code first before checking the condition, and the second construct tests the condition first before it executes the code (if ever). I take it Matz prefers to keep the second construct because it matches one line construct of if statements.

I never liked the second construct even for if statements. In all other cases, the computer executes code left-to-right (eg. || and &&) top-to-bottom. Humans read code left-to-right top-to-bottom.

I suggest the following constructs instead:

if <cond> then <one_line_code>      # matches case-when-then statement

while <cond> then <one_line_code>

<one_line_code> while <cond>

begin <multiple_line_code> end while <cond> # or something similar but left-to-right

I don't know if those suggestions will parse with the rest of the language. But in any case I prefere keeping left-to-right execution as well as language consistency.

Boris
  • 71
  • 1
  • 11
jds
  • 69
  • 1
  • 1
3
a = 1
while true
  puts a
  a += 1
  break if a > 10
end
  • 3
    this looks a bit like a goto. The code obfusticate your intention. – Gerhard Nov 25 '10 at 13:14
  • Looks great to me, except `while true` can be replaced with `loop do`. – David Winiecki May 03 '14 at 22:14
  • @DavidWiniecki, indeed, `while true` can be replaced with `loop do`. But I tested both constructs with a lot of iterations inside the loop, and discovered that `while true` is at least 2x _faster_ than `loop do`. Can't explain the difference, but it's definitely there. (Discovered while testing Advent of Code 2017, day 15.) – Jochem Schulenklopper Dec 15 '17 at 21:34
2

Here's another one:

people = []
1.times do
  info = gets.chomp
  unless info.empty? 
    people += [Person.new(info)]
    redo
  end
end
Moray
  • 37
  • 1
  • I prefer this as the `unless` is right up front and I don't read through a bunch of code (which could be more than shown here) only to find a 'dangling' `unless` at the end. It's a general principle in code that modifier and conditions are easier to use when they are 'up front' like this. – Michael Durrant Mar 08 '12 at 18:05
  • I sometimes wish we coders had to pay cash for every extra compare. And that how it "looks" to us was less important than how it looks to the thing that uses it a million times a day. – baash05 Dec 16 '12 at 22:15
-3
ppl = []
while (input=gets.chomp)
 if !input.empty?
  ppl << input
 else
 p ppl; puts "Goodbye"; break
 end
end