In addition to conditionals, loops, and iterators, Ruby supports a number of statements that alter the flow-of-control in a Ruby program. These statements are:
return
break
next
Causes a loop (or iterator) to skip the rest of the current iteration and move on to the next iteration.
redo
retry
Restarts an iterator, reevaluating the entire expression. The
retry
keyword can also be used
in exception handling, as we’ll see later in the chapter.
throw/catch
A very general control structure that is named like
and works like an exception propagation and handling mechanism.
throw
and catch
are not Ruby’s primary exception
mechanism (that would be raise
and rescue
, described later in
this chapter). Instead, they are used as a kind of multilevel or
labeled break
.
The subsections that follow describe each of these statements in detail.
The return
statement causes
the enclosing method to return to its caller. If you know C, Java, or
a related language, you probably already have an intuitive
understanding of the return
statement. Don’t skip this section, however, because the behavior of
return
within a block may not be
intuitive to you.
return
may optionally be
followed by an expression, or a comma-separated list of expressions.
If there is no expression, then the return value of the method is
nil
. If there is one expression,
then the value of that expression becomes the return value of the
method. If there is more than one expression after the return
keyword, then the return value of the
method is an array containing the values of those expressions.
Note that most methods do not require the return
statement. When flow-of-control
reaches the end of a method, the method automatically returns to its
caller. The return value in this case is the value of the last
expression in the method. Most Ruby programmers omit return
when it is not necessary. Instead of
writing return x
as the last line
of a method, they would simply write x
.
return
is useful if you want
to return from a method prematurely, or if you want to return more
than one value. For example:
# Return two copies of x, if x is not nil def double(x) return nil if x == nil # Return prematurely return x, x.dup # Return multiple values end
When first learning about Ruby blocks, it is natural to think of
them as some kind of nested function or mini-method. And if you think
of them this way, you might expect return
simply to cause the block to return
to the iterator that yielded to it. But blocks are not methods, and
the return
keyword does not work
this way. In fact, return
is
remarkably consistent; it always causes the enclosing method to
return, regardless of how deeply
nested within blocks it is.[*]
Note that the enclosing method is not the same thing as the
invoking method. When the return
statement is used in a block, it does not just cause the block to
return. And it does not just cause the iterator that invokes the block
to return. return
always causes the
enclosing method to return. The enclosing method, also called the
lexically enclosing method, is the method that
the block appears inside of when you look at the source code. Figure 5-2 illustrates the behavior of the return
statement in a block.
The following code defines a method that uses return
to return from inside a block:
# Return the index of the first occurrence of target within array or nil # Note that this code just duplicates the Array.index method def find(array, target) array.each_with_index do |element,index| return index if (element == target) # return from find end nil # If we didn't find the element, return nil end
The return
statement in this
code does not just cause the block to return to the iterator that
invoked it. And it does not just cause the each_with_index
iterator to return. It
causes the find
method to return a
value to its caller.
When used within a loop, the break
statement transfers control out of the
loop to the first expression following the loop. Readers who know C, Java, or a similar language will
already be familiar with the use of break
in a loop:
while(line = gets.chop) # A loop starts here break if line == "quit" # If this break statement is executed... puts eval(line) end puts "Good bye" # ...then control is transferred here
When used in a block, break
transfers control out of the block, out of the iterator that invoked
the block, and to the first expression following the invocation of the
iterator. For example:
f.each do |line| # Iterate over the lines in file f break if line == "quit " # If this break statement is executed... puts eval(line) end puts "Good bye" # ...then control is transferred here
As you can see, using break
inside a block is lexically the same as using it inside a loop. If you
consider the call stack, however, break
in a block is more complicated because
it forces the iterator method that the block is associated with to
return. Figure 5-3
illustrates this.
Note that unlike return
,
break
never causes the lexically
enclosing method to return. break
can only appear within a lexically enclosing loop or within a block.
Using it in any other context causes a LocalJumpError
.
Recall that all syntactic constructs in Ruby are expressions,
and all can have a value. The break
statement can specify a value for
the loop or iterator it is breaking out of. The break
keyword may be followed by an
expression or a comma-separated list of expressions. If break
is used with no expression, then the
value of the loop expression, or the return value of the iterator
method, is nil
. If break
is used with a single expression,
then the value of that expression becomes the value of the loop
expression or the return value of the iterator. And if break
is used with multiple expressions,
then the values of those expressions are placed into an array, and
that array becomes the value of the loop expression or the return
value of the iterator.
By contrast, a while
loop
that terminates normally with no break
always has a value of nil
. The return value of an iterator that
terminates normally is defined by the iterator method. Many
iterators, such as times
and
each
, simply return the object on
which they were invoked.
The next
statement causes a loop or iterator to end the current iteration
and begin the next. C and Java programmers know this control structure
by the name continue
. Here
is next
in a loop:
while(line = gets.chop) # A loop starts here next if line[0,1] == "#" # If this line is a comment, go on to the next puts eval(line) # Control goes here when the next statement is executed end
When next
is used within a
block, it causes the block to exit immediately, returning control to
the iterator method, which may then begin a new iteration by invoking
the block again:
f.each do |line| # Iterate over the lines in file f next if line[0,1] == "#" # If this line is a comment, go to the next puts eval(line) # Control goes here when the next statement is executed end
Using next
in a block is
lexically the same as using it in a while
, until
, or for/in
loop. When you consider the calling
sequence, however, the block case is more complicated, as Figure 5-4 illustrates.
next
may only be used within
a loop or a block; it raises a LocalJumpError
when used in any other
context.
Like the return
and
break
keywords, next
may be used alone, or it may be
followed by an expression or a comma-separated list of expressions.
When next
is used in a loop, any
values following next
are
ignored. In a block, however, the expression or expressions become
the “return value” of the yield
statement that invoked the block. If next
is not followed by an expression,
then the value of the yield
is
nil
. If next
is followed by one expression, then
the value of that expression becomes the value of the yield
. And if next
is followed by a list of expressions,
then the value of the yield
is an
array of the value of those expressions.
In our earlier discussion of the return
statement, we were careful to
explain that blocks are not functions, and that the return
statement does not make a block
return to the iterator that invoked it. As you can see, this is
exactly what the next
statement
does. Here is code where you might use it in this way:
squareroots = data.collect do |x| next 0 if x < 0 # Return 0 for negative values Math.sqrt(x) end
Normally, the value of a yield
expression is the value of the last
expression in the block. As with the return
statement, it is not often
necessary to explicitly use next
to specify a value. This code could also have been written like
this, for example:
squareroots = data.collect do |x| if (x < 0) then 0 else Math.sqrt(x) end end
The redo
statement restarts the current iteration of a loop or iterator. This is
not the same thing as next
.
next
transfers control to the end
of a loop or block so that the next iteration can begin, whereas
redo
transfers control back to the
top of the loop or block so that the iteration can start over. If you
come to Ruby from C-like languages, then redo
is probably a new control structure for
you.
redo
transfers control to the
first expression in the body of the loop or in a block. It does not
retest the loop condition, and it does not fetch the next element from
an iterator. The following while
loop would normally terminate after three iterations, but a redo
statement makes it iterate four
times:
i = 0 while(i < 3) # Prints "0123" instead of "012" # Control returns here when redo is executed print i i += 1 redo if i == 3 end
redo
is not a commonly used
statement, and many examples, like this one, are contrived. One use,
however, is to recover from input errors when prompting a user for
input. The following code uses redo
within a block for this purpose:
puts "Please enter the first word you think of" words = %w(apple banana cherry) # shorthand for ["apple", "banana", "cherry"] response = words.collect do |word| # Control returns here when redo is executed print word + "> " # Prompt the user response = gets.chop # Get a response if response.size == 0 # If user entered nothing word.upcase! # Emphasize the prompt with uppercase redo # And skip to the top of the block end response # Return the response end
The retry
statement
is normally used in a rescue
clause to reexecute a block of code
that raised an exception. This is described in retry in a rescue clause. In Ruby 1.8, however, retry
has another
use: it restarts an iterator-based iteration (or any method
invocation) from the beginning. This use of the retry
statement is extremely rare, and it
has been removed from the language in Ruby 1.9. It should, therefore,
be considered a deprecated language feature and should not be used
in new code.
In a block, the retry
statement does not just redo the current invocation of the block; it
causes the block and the iterator method to exit and then reevaluates
the iterator expression to restart the iteration. Consider the
following code:
n = 10 n.times do |x| # Iterate n times from 0 to n–1 print x # Print iteration number if x == 9 # If we've reached 9 n -= 1 # Decrement n (we won't reach 9 the next time!) retry # Restart the iteration end end
The code uses retry
to
restart the iterator, but it is careful to avoid an infinite loop. On
the first invocation, it prints the numbers 0123456789
and then restarts. On the second
invocation, it prints the numbers 012345678
and does not restart.
The magic of the retry
statement is that it does not retry the iterator in exactly the same
way each time. It completely reevaluates the iterator expression,
which means that the arguments to the iterator (and even the object on
which it is invoked) may be different each time the iterator is
retried. If you are not used to highly dynamic languages like Ruby,
this reevaluation may seem counterintuitive to you.
The retry
statement is not
restricted to use in blocks; it always just reevaluates the nearest
containing method invocation. This means that it can be used (prior to
Ruby 1.9) to write iterators like the following that works like
a while
loop:
# This method behaves like a while loop: if x is non-nil and non-false, # invoke the block and then retry to restart the loop and test the # condition again. This method is slightly different than a true while loop: # you can use C-style curly braces to delimit the loop body. And # variables used only within the body of the loop remain local to the block. def repeat_while(x) if x # If the condition was not nil or false yield # Run the body of the loop retry # Retry and re-evaluate loop condition end end
throw
and catch
are Kernel
methods that define a control structure that can be thought of
as a multilevel break
. throw
doesn’t just break out of the current
loop or block but can actually transfer out any number of levels,
causing the block defined with a catch
to exit. The catch
need not even be in the same method as
the throw
. It can be in the calling
method, or somewhere even further up the call stack.
Languages like Java and JavaScript allow loops to be named or
labeled with an arbitrary prefix. When this is done, a control
structure known as a “labeled break
” causes the named loop to exit. Ruby’s
catch
method defines a labeled
block of code, and Ruby’s throw
method causes that block to exit. But throw
and catch
are much more general than a labeled
break
. For one, it can be used with any kind of statement and
is not restricted to loops. More profoundly, a throw
can propagate up the call stack to
cause a block in an invoking method to exit.
If you are familiar with languages like Java and JavaScript,
then you probably recognize throw
and catch
as the keywords those
languages use for raising and handling exceptions. Ruby does
exceptions differently, using raise
and rescue
, which we’ll learn about
later in this chapter. But the parallel to exceptions is intentional.
Calling throw
is very much like
raising an exception. And the way a throw
propagates out through the lexical
scope and then up the call stack is very much the same as the way an
exception propagates out and up. (We’ll see much more about exception
propagation later in the chapter.) Despite the similarity to
exceptions, it is best to consider throw
and catch
as a general-purpose (if perhaps
infrequently used) control structure rather than an exception
mechanism. If you want to signal an error or exceptional condition,
use raise
instead of throw
.
The following code demonstrates how throw
and catch
can be used to “break out” of nested
loops:
for matrix in data do # Process a deeply nested data structure. catch :missing_data do # Label this statement so we can break out. for row in matrix do for value in row do throw :missing_data unless value # Break out of two loops at once. # Otherwise, do some actual data processing here. end end end # We end up here after the nested loops finish processing each matrix. # We also get here if :missing_data is thrown. end
Note that the catch
method
takes a symbol argument and a block. It executes the block and returns
when the block exits or when the specified symbol is thrown. throw
also expects a symbol as its argument
and causes the corresponding catch
invocation to return. If no catch
call matches the symbol passed to throw
, then a NameError
exception is raised. Both catch
and throw
can be invoked with string arguments
instead of symbols. These are converted internally to symbols.
One of the features of throw
and catch
is that they work even
when the throw
and catch
are in different methods. We could
refactor this code to put the innermost loop into a separate method,
and the control flow would still work correctly.
If throw
is never called, a
catch
invocation returns the value
of the last expression in its block. If throw
is called, then the return value of
the corresponding catch
is, by
default, nil
. You can, however,
specify an arbitrary return value for catch
by passing a second argument to
throw
. The return value of catch
can help you distinguish normal
completion of the block from abnormal completion with throw
, and this allows you to write code
that does any special processing necessary to respond to the throw
.
throw
and catch
are not commonly used in practice. If
you find yourself using catch
and
throw
within the same method,
consider refactoring the catch
into
a separate method definition and replacing the throw with a return
.
[*] We’ll see an exception when we consider lambdas in Return in blocks, procs, and lambdas. A lambda is a kind of a function created from a block, and
the behavior of return
within a
lambda is different from its behavior in an ordinary block.
3.145.173.199