Altering Control Flow

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

Causes a method to exit and return a value to its caller.

break

Causes a loop (or iterator) to exit.

next

Causes a loop (or iterator) to skip the rest of the current iteration and move on to the next iteration.

redo

Restarts a loop or iterator from the beginning.

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.

return

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 return statement in a block

Figure 5-2. 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.

break

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.

The break statement in a block

Figure 5-3. The break statement in a block

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.

break with a value

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.

next

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.

The next statement in a block

Figure 5-4. The next statement in a block

next may only be used within a loop or a block; it raises a LocalJumpError when used in any other context.

next and block value

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

redo

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

retry

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

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.69.157