Flow Control

From RubySpec

Jump to: navigation, search

Ruby provides most standard flow-control keywords as well as a few unusual (but frequently useful) keywords of its own.

Contents

Flow control expressions

The conditionals comprise if, unless and case. Loop operations comprise while, until, and for. The use of blocks (TODO: xref) allows easy creation of user- or library-defined flow control operations, see for example loop and each (TODO: xrefs).

In Ruby, conditional and looping ``statements are actually expressions, and return values.

Truth values

TODO: this may be in the wrong place, because it also applies to conditional expressions (? :). Maybe move it somewhere else?

Syntax

condition: expression

A number of the flow-control expressions include conditions. A condition is an expression that is required to evaluate to a truthvalue, which is either true or false. A truthvalue is false if and only if its value is either false or nil. Anything else is considered to be true, including 0, [], and any other empty values.

Conditionals

if..elsif..end / unless..end

Ruby features the standard if conditional in both expression and expression modifier form. unless condition is exactly equivalent to if not condition in all contexts.

An if/unless expression returns the value of its last expression.

Block form

Syntax

 if condition [then]
   code
 [elsif condition [then]
   code]
 [else
   code]
 end
 unless condition
   code
 [else
   code]
 end

Explanation

The basic expression form of if looks like this, and must end with an end:

 if some_condition_expression
   # This section executed if some_condition_expression is true
 end

The else-keyword can be used to provide a section that will be executed if the condition expression evaluates to false. Only one else may appear in an if-expression:

 if some_condition_expression
   # This section executed if some_condition_expression is true
 else
   # This section executed if some_condition_expression is false
 end

The elsif-keyword can be used to create multiple mutually exclusive conditionals. If the conditional in the initial if is not true, then each elsif conditional is evaluated in descending order until one is true. If none is, the else-section is executed if there is one:

 if cond_one
   # cond_one is true
 elsif cond_two
   # cond_one is not true and cond_two is true
 elsif cond_three
   # cond_one is not true and cond_two is not true and cond_three is true
 else
   # cond_one, cond_two and cond_three are all not true
 end

With unless, it is not possible to define elsifs, just else.

The if and unless expressions may have an optional then added, but it is usually elided.

 if some_condition_expression then
   # ...
 elsif some_other_condition_expression then
   # ...
 end
 unless some_condition_expression then
   # ...
 else some_other_condition_expression then
   # ...
 end

However, then is necessary if you want to keep the if or unless expression on a single line.

 if some_condition_expression then do_something end
 unless some_condition_expression then do_something end

Ruby accepts a postfix notation for conditionals if and unless if you keep the if expression in a single line:

 do_something if some_condition_expression 
 do_something_else unless some_other_condition_expression

case...when...else...end

A case statement matches the result of the case expression against each of the results of the when expression using the === matching operator. There can be any number of when clauses, but only the body of the first match is evaluated. If no when clauses match, and the programmer has provided an else clause, that body is evaluated. If there is no match and no else clause, the case expression returns nil.

 case degrees_in_kelvin
 when 0..273
   puts "solid"
 when 274..372
   puts "liquid"
 else
   puts "gas"
 end

The case...when..else..end construct determines matching with the === method. The default === method inherited from Object is an equality test using equal? which performs strict object equality. This method is implemented as an alias, however, so redefining the equal? method on a subclass will not change the behavior of the default inherited === method.

Many core classes like RegExp and Range provide === methods which match whole groups of objects.

 case "hello"
 when /^ye/
   puts "yee-haw"    
 when /^he/
   puts "hee-haw"
 end

When the match is performed, the object from the when expression is the invocant and the object from the case expression is the argument. From the example above, the successful match is found with the following expression: /^he/ === "hello".

Like most Ruby special forms, the case...when...else...end expression returns its result. This will be the result of the last expression evaluated inside the block

 puts case x
      when 0...10
        puts "this is not the value"
        0 # but this will be if x is in between 0 and 10
      when 10...100
        1
      when 100...1000
        2
      end

Like the if statement, the when clause can take an optional then to separate it from the result expression. Also like if, the then keyword becomes required when the result is on the same line as the when clause:

 case degrees_in_kelvin
     when 0..273 then "solid"
     when 274..372 then "liquid"
     else "gas"
 end

Looping

while/until

A while executes the body as long as the condition is true.

 while test
    puts "hello #{i}"
 end

Its counterpart, until, iterates while the loop condition is false; thus the above is equivalent to

 until !test
    puts "hello#{i}"
 end

Syntax

while-expression: while condition (do | newline) expressions end
until-expression: until condition (do | newline) expressions end

Semantics

A while-expression is equivalent to the following pseudocode, assuming a fictitious goto statement.

returnvalue = nil;
L_cond: if !condition then goto L_finished;
L_redo:
expressions
goto L_cond
L_finished:

The identifiers returnvalue, L_cond, L_redo and L_finished are replaced by identifiers not used elsewhere in the program.

An until-expression is equivalent to the following pseudocode.

returnvalue = nil;
L_cond: if condition then goto L_finished;
L_redo:
expressions
goto L_cond
L_finished:

The identifiers returnvalue, L_cond, L_redo and L_finished are replaced by identifiers not used elsewhere in the program.

The value returned by a while- or until-expression is nil unless a break is executed inside the loop (TODO: xref).

TODO: I'm not wildly keen about using gotos here. It seems less offensive than defining loops in terms of recursive functions, though, so that's why I did it. I think the expansions I've shown here need begin/end around them to get the scopes right. End of TODO.

loop

TODO: maybe this should be in the description of Kernel, with a cross-reference from here?

The Kernel method loop takes a block and no arguments. The block is invoked repeatedly until terminated by one of the standard ways iteration is halted (break, return, raise, throw, continuations). Iteration keywords behave as expected.

 loop do
   puts "hi"
 end

For example, break can be used to add termination conditions.

 loop do
   break if STDIN.eof?
   puts STDIN.gets
 end

It's important to remember that loop is not a Ruby keyword. It is a method. This means among other things that it can be redefined or overwritten.

Many languages like Smalltalk implement all their control structures using methods and blocks, but this isn't an option for Ruby which lacks a concise syntax for methods that take multiple block parameters.

for..in

The for..in construct can be used to iterate over any object that responds to the each method. The following two code snippets showing iteration over an Array are essentially equivalent:

 for item in %w{Item1 Item2 Item3 Item4}
   puts item
 end
 %w{Item1 Item2 Item3 Item4}.each do |item|
   puts item
 end

The one difference to note is the scope of the iterator variable. In the case of the for..in construct, the iterator variable will be created/modified in the local scope regardless of whether or not it had previously existed. In the case of the each method, the iterator variable will not be created in the local scope. However, if the iterator variable is the name of a variable that already exists in the local scope, that variable will be modified.

Multiple variables may be used with the for..in construct just as with the each method as the following example to iterate over a Hash shows:

 for key, value in {'item1' => 1, 'item2' => 2, 'item3' => 3}
   puts "#{key} => #{value}"
 end

TODO: precise syntax and semantics

next, break, redo

Inside the body of a loop, the special control transfer instructions next, break, and redo can be used to control looping.

The next construct causes execution to be transferred back to the top of the enclosing loop with the loop condition being re-evaluated. The following example illustrates how this could be used to skip comment lines when parsing a File. When a line beginning with a # character is encountered, the next construct causes the for loop to jump back to the top and start processing the next line from the file.

The redo construct causes execution to be transferred back to the top of the enclosing loop just like the next construct. However, unlike the next construct, the redo construct does not cause the loop condition to be re-evaluated. The following example illustrates how this could be used to re-process a line when parsing a File. When a line beginning with ERR: is found, the redo construct causes the for loop to jump back to the top and re-process the current line after ERR has been replaced by ERROR (not how you would do it... but I can't think of a better example for redo at the moment).

The break construct causes the enclosing loop to terminate immediately. The following example illustrates how this could be used to terminate a loop if an error line is found when parsing a File. When a line starting with ERROR: is found, the break construct is used to terminate the for loop.

 File.open('test.txt') do |f|
   for line in f
     next if /^#/ =~ line
     break if /^ERROR:/ =~ line
     if /^ERR:/ =~ line
       line.gsub!(/^ERR/, 'ERROR')
       redo
     end
     puts line
   end
 end

Syntax

 break-expression: break [expression]
 next-expression: next
 redo-expression: redo

Semantics

A break-expression inside a loop is executed as though it were

returnvalue = expression; goto L_finished

If no expression is provided, no assignment is executed (thus the loop's return value is nil).

A next expression inside a loop is executed as though it were

goto L_cond

A redo expression inside a loop is executed as though it were

goto L_redo


Exception handling

begin...rescue...else...ensure...end

A begin...end block can offset a section of code. It is typically used for exception handling.

TODO: there are at least 3 different uses for begin/end: creating local scope, begin...end while condition, and exception handling. Maybe they should be presented separately? Suggestion: syntax as in following block

begin-expression: begin expressions [exception-machinery] end
exception-machinery: rescue except-spec [else expressions] [ensure expressions]

End of TODO

begin
  puts "a bare begin/end alone isn't terribly useful'
end
begin
  puts "however when used with a rescue, exception handling results"
  raise Exception.new("my exception")
rescue Exception => e
  puts e.backtrace
end

A begin...end block can stand alone, can be written with one or more rescue blocks, and can contain one or both of the optional else and ensure blocks.

begin
  puts "ensure blocks are useful for cleaning up resources regardless of errors that may occur"
  f = File.open("myfile")
  raise Exception.new
rescue NameError => n
  # no NameError raised, so we'll not get here
else
  # we'll only get here if the main block executes without exception
ensure
  # but we'll always get here, unless the interpreter or thread terminates
  f.close
end

Note that Ruby's standard scoping for local variables holds for begin...end blocks as well: there is only one scope in a given method (not counting block-scoped variables).

The anonymous form of rescue only catches exceptions that descend from StandardError, as follows:

begin
  eval "\"" # raises a SyntaxError
rescue => e
  # The SyntaxError will not be caught
end

Compare to:

begin
  raise NameError.new("Some name error")
rescue => e
  # The NameError will be caught
end

Other

TODO: would it make sense to describe and define blocks here? Or maybe earlier in this section, before each?

throw-catch

callcc

yield

Ruby methods can optionally be supplied a block on invocation. This block can optionally be made available as a Proc object using the & prefix on the final parameter of a method, but remains invocable at all times through the yield keyword.

The yield keyword looks syntactically just like a method call on the current object. It takes a list of parameters just like method calls and returns a value.

 yield(1, 2)
 yield 3
 x = yield(4)

Even if the block argument is named in a method, code in that method can still invoke the block using yield, instead of calling the Proc object.

 def both_ways(&block)
   block.call(1)
   yield(1)
 end

However, these two forms are not identical. For example, redefinitions of Proc#call will affect the first invocation, but not the second.

Much like blocks can be named and captured as Proc objects inside a method, Procs can also be inserted into the block slot at method call time using the & prefix on the last argument of the call.

 def only_uses_yield
   yield
 end
 
 a_proc = proc{ puts "called" }
 only_uses_yield(&a_proc)

Does anyone know if this mechanism depends on Proc#call or extracts the block from the Proc?

Personal tools