Video transcript & code
Most of the time when we write a loop in a program, we have a specific end state in mind. For instance, we want to iterate all of the elements of a collection. However, there are exceptions to this rule.
Consider this small program. It implements a trivial server which simply echoes messages back, reversed. If we start it up in one shell and then telnet into the appropriate port from another, we can see that it works well.
require 'socket' server = TCPServer.new 9001 while true do client = server.accept input = client.readline.chomp client.puts input.reverse client.close end
In this code we can see a a pattern that's very common at the top-level of applications: a main-loop. Unlike most loops, main loops are usually intended to loop forever, or at least until interrupted by some signal.
In order to loop forever, this program uses a construct that I often run across in Ruby programs. It uses a
while loop with a condition of
This functions perfectly well as an infinite loop. And in many programming languages, this or something very like it is the idiomatic way to write an infinite loop.
But Ruby is not other programming languages. Ruby puts a great deal of stock in code that expresses the intent of the programmer. So while this works, and while it is the solution that many programmers naturally think of for a main loop, it is not the idiomatic way to write an infinite loop.
Instead, if we want some code to loop forever in Ruby, we should use the
Kernel method that exists precisely for that purpose, called
require 'socket' server = TCPServer.new 9001 loop do client = server.accept input = client.readline.chomp client.puts input.reverse client.close end
This is one of those little changes that makes a difference, at least to me. When I see a
while loop, a part of my brain immediately starts thinking, "OK, this is a loop, let's see what the condition is…". Then I read the
true part, and think: "oh, it's a loop forever".
Whereas when I see
loop invoked, I instantly think "ah, an infinite loop", and move on to read the loop body without any wasted cycles.
Whether or not you already use
#loop, you might wonder if it differs in any ways from a
while true loop. As far as I know, there are two differences.
First off: like all other standard Ruby methods that deal with iteration, when invoked without a block
#loop returns an
Enumerator that is returned is a kind of "infinite bucket of nothing". It can be advanced forever, and will always return
nil. It's a bit like reading from the
/dev/zero device in Linux.
enum = loop # => #<Enumerator: main:loop> enum.next # => nil enum.next # => nil enum.next # => nil
I was tempted to show some an example of how we could use this infinite nil enumerator to perform some abstract programming tricks, but frankly the examples I could come up with were a bit contrived. If you have an idea of a practical way to use this enumerator, please send it in.
The other difference between
while true is slightly more relevant. Like any block, the block passed to
loop has its own variable scope. Whereas a
while loop doesn't introduce any new scopes.
To understand what this means, lets construct a while loop which assigns a variable internally, and then immediately breaks out. When we examine the variable, we are able to read the value that was set inside the loop.
while true foo = 42 break end foo # => 42
But when we convert the loop to use the
#loop method, this no longer works. Ruby can't find the block-local variable in the outer context.
loop do foo = 42 break end foo # => # ~> NameError # ~> undefined local variable or method `foo' for main:Object # ~> # ~> xmptmp-in254795zM.rb:6:in `<main>'
This difference probably isn't going to have an effect on most programs. But I instinctively prefer the tidiness of variables that don't "leak" outside of the loop context. After all, most such variables are created in order to hold intermediate values inside the loop proper, and so using them after the loop ends is probably a result of a mistake.
If there really is a variable we want to set within the loop and then use afterwards, we can always explicitly declare the variable before the loop starts. Then Ruby will assign to the existing outer variable, rather than making one that's local only to the block.
foo = nil loop do foo = 42 break end foo # => 42
I find this to be more intention-revealing, which was the whole point of switching to the
#loop method in the first place.
Anyway, that's the
#loop method, and that's all for today. Happy hacking!