Basic Ruby Syntax and Semantics

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


Bring forth that ruby gem of Badakhshan,That heart's delight, that balm of Turkestan….

The Rubaiyat, Omar Khayyam (trans. E. H. Whinfield)

In the previous pages, you have already seen that Ruby is a pure, dynamic, OOP language. Let's now look briefly at some other attributes before summarizing the syntax and semantics.

Ruby is a scripting language. This should not be construed as meaning that it is not powerful. It can serve as a "glue language" in the tradition of KornShell and others, or it can serve as an environment for creating larger self-contained applications. Readers who are interested in the industry trend toward scripting languages should refer to John Ousterhout's article "Scripting: Higher-Level Programming for the 21st Century" in the March 1998 issue of IEEE Computer. (Ousterhout is the creator of the Tcl language.)

Ruby is an interpreted language. Of course, there may be later implementations of a Ruby compiler for performance reasons, but we maintain that an interpreter yields great benefits not only in rapid prototyping but in the shortening of the development cycle overall.

Ruby is an expression-oriented language. Why use a statement when an expression will do? This means, for instance, that code becomes more compact as the common parts are factored out and repetition is removed.

Ruby is a very high-level language (VHLL). One principle behind the language design is that the computer should work for the programmer rather than vice versa. The "density" of Ruby means that sophisticated and complex operations can be carried out with relative ease as compared to lower-level languages.

Having said all that, let's look more closely at Ruby. This section and the rest of the chapter concentrate on the Ruby language itself. As mentioned before, this is only a quick summary, so if you haven't learned it somewhere else, you won't learn it here.

Our first look at Ruby will not concentrate on the language's more complex features. These are covered in the next two sections. Here, we are concerned with the overall look and feel of the language and some of its terminology. We'll briefly examine the nature of a Ruby program before looking at examples.

To begin with, Ruby is essentially a line-oriented languagemore so than languages such as C but not so much as antique languages such as FORTRAN. Tokens can be crowded onto a single line as long as they are separated by whitespace, as needed. Statements may occur more than one to a line if they are separated by semicolons; this is the only time the terminating semicolon is really needed. A line may be continued to the next line by ending it with a backslash or by letting the parser know that the statement is not completefor example, by ending a line with a comma.

There is no main program as such; execution proceeds in general from top to bottom. In more complex programs, there may be numerous definitions at the top, followed by the (conceptual) main program at the bottom. However, even in that case, execution proceeds from the top down because definitions in Ruby are executed.

Keywords and Identifiers

The keywords (or reserved words) in Ruby typically cannot be used for other purposes. These are as follows:

BEGIN END alias and begin
break case class def defined
do else elsif end ensure
false for if in module
next nil not or redo
rescue retry return self super
then true undef unless until
when while yield   


Variables and other identifiers normally start with an alphabetic letter or a special modifier. The basic rules are as follows:

  • Local variables (and pseudo-variables such as self and nil) begin with a lowercase letter.

  • Global variables begin with a dollar sign ($).

  • Instance variables (within an object) begin with an "at" sign (@).

  • Class variables (within a class) begin with two "at" signs (@@).

  • Constants begin with capital letters.

For purposes of forming identifiers, the underscore (_) may be used as a lowercase letter. Special variables starting with a dollar sign (such as $1 and $/) are not dealt with here.

The following list provides some examples:

  • Local variables: alpha, _ident, some_var

  • Pseudo-variables: self, nil, __FILE__

  • Constants: K6chip, Length, LENGTH

  • Instance variables: @foobar, @thx1138, @NOT_CONST

  • Class variable: @@phydeaux, @@my_var, @@NOT_CONST

  • Global variables: $beta, $B12vitamin, $NOT_CONST

Comments and Embedded Documentation

Comments in Ruby begin with a pound sign (#) outside of a string or character constant and proceed to the end of the line:

 

 x = y + 5 # This is a comment. # This is another comment. print "# But this isn't." 

Embedded documentation is intended to be retrieved from the program text by an external tool such as RDTOOL. From the point of view of the interpreter, it is like a comment and can be used as such. Given two lines starting with =begin and =end, everything between those lines (inclusive) is ignored by the interpreter:

 

 =begin The purpose of this program is to cure cancer and instigate world peace. =end 

Constants, Variables, and Types

In Ruby, variables do not have types, but the data they contain still has types. The simplest data types are numeric, character, and string.

Some numeric constants are shown in the following list:

  • Integer: 237

  • Integer (with sign): -123

  • Integer (with underscore spacing): 1_048_576

  • Octal integer: 0377

  • Hexadecimal integer: 0xBEEF

  • Floating point: 3.14159

  • Floating point (scientific notation): 6.02e23

Character constants in Ruby actually evaluate to integers according to the ASCII code and are therefore interchangeable with the corresponding integer constants. Here are some character constants:

  • Lowercase x (120): ?x

  • Newline: ?\n

  • Backslash: ?\\

  • Ctrl+D: ?\cd

  • Ctrl+X: ?\C-x

  • Meta+X (x ORed with 0x80): ?\M-x

  • Meta+Ctrl+X: ?\M-\C-x

Note that all these forms (and some others) can also be embedded in strings, as you'll see shortly.

Ruby has a wealth of notations available for representing strings; different ones may be convenient in different situations. Most commonly, a string constant in Ruby is enclosed between double quotes, as in C.

It is possible to embed "escaped" character constants in a Ruby string in order to express control characters and the like:

 

 "This is a single line.\n" "Here are three tabs\ t\t\tand then more text." "A backslash (\\) must be doubled." 

It is also possible to embed variables or even expressions inside these strings. The pound sign (#) is used to signal that this is being done; typically the variable or expression is enclosed in braces, but they may be omitted if the expression consists of a single variable beginning with $ or @. Here are examples:

 

 "The tax rate is #{ taxrate} ." "Hello, #@yourname; my name is #{ myname} ." "The sum is #{ a+b+c} ." "#$num1 times #$num2 equals #{ $num1*$num2} ." 

A single-quoted string in Ruby is the same except that no expression substitution is performed and no backslash processing is done, except \\ and \'. Single-quoted strings are useful when the strings are to be used more "as is," without the special interpretations. Here are examples:

 

 'The notation #{ alpha}  will not be expanded.' 'We can embed the \\  (backslash) character' 'or the \' (single quote) character.' 

For cases where the strings contain punctuation that would normally have to be escaped, there is a more general form of quote. The percent sign (%) introduces a string delimited according to rules determined by the character following the percent sign. Basically this character may be a lowercase q, an uppercase Q, a brace or parenthesis, or some other character. In the first two cases, there is still a delimiter character following the letter. We'll discuss each case briefly.

A %q string is a generalized single-quoted string; as such, there is no expression substitution and minimal backslash processing. The delimiter may be any character, including newline. If an opening brace or parenthesis is used, the corresponding closing brace or parenthesis closes the string; otherwise, the same character opens and closes the string. Here are examples:

 

 %q(The notation #{ alpha}  will not be expanded.) %q{ We can embed \\  and \}  in this string.} %q/These characters are not special: " ' # () { } / 

A %Q string is a generalized double-quoted string, meaning that substitution and backslash processing both occur as they normally would. The delimiters behave as with the %q string. Here are examples:

 

 %Q(We can embed tabs \t\t and newlines and so on.) %Q/Here, these characters are not special: () " '/ %Q("Hello, #{ name} ," I said to her.) %Q(He said, "She said, 'Hello.'") 

The q or Q may be left out entirely so that the delimiter immediately follows the percent sign. This delimiter obviously may not be q or Q but also may not be r, w, or x, for reasons you'll see shortly. In this case, the string once again acts like a double-quoted string. Here are examples:

 

 %(The variable alpha = #{ alpha} .) %/Tab \t  Carriage return \r  Newline \n/ %{ Using a brace makes substitution hard: #\{ beta\} } %<Less-than greater-than will also work.> %[As will square brackets.] 

Note once again that the closing delimiter is the same as the opening delimiter for most characters, but a "paired character" used as a delimiter requires the opposite paired character to close the string. The paired characters are parentheses, brackets, braces, and the so-called "angle brackets": (), [], {}, and <>, respectively. Note, however, that the grave accent (`) and single quote (') are not paired characters as some might think.

A special kind of string is worth mentioning here that's primarily useful in small scripts used to glue together larger programs. The command output string will be sent to the operating system as a command to be executed, whereupon the output of the command is substituted back into the string. The simple form of this string uses the grave accent (sometimes called a back tick or back quote) as a beginning and ending delimiter; the more complex form uses the %x notation:

 

 `whoami` `ls l` %x[grep i meta *.html | wc l] 

Regular expressions in Ruby look similar to character strings, but they are used differently. Many operations in Ruby make sense with regular expressions but not with strings.

For those familiar with Perl, regular expression handling is similar in Ruby. Incidentally, we'll use the abbreviation regex throughout the remainder of the book; many abbreviate it as regexp, but that is not as pronounceable.

The typical regex is delimited by a pair of slashes; the %r form can also be used. Here are some simple regular expressions:

  • /Ruby/ Matches the single word Ruby

  • /[Rr]uby/ Matches Ruby or ruby

  • /^abc/ Matches an instances of abc at the beginning of a line

  • %r(xyz$) Matches an instance of xyz at the end of a line

  • %r|[0-9]*| Matches any sequence of (zero or more) digits

It is also possible to place a modifier, consisting of a single letter, immediately after a regex. The modifiers are as follows:

  • i Ignores case in regex

  • o Performs expression substitution only once

  • m Multiline mode (dot matches newline)

  • x Extended regex (allows whitespace and comments)

To complete our introduction to regular expressions, here's a list of the most common symbols and notations available:

  • ^ Beginning of a line or string

  • $ End of a line or string

  • . Any character except newline (unless POSIX)

  • \w Word character (digit, letter, or underscore)

  • \W Non-word character

  • \s Whitespace character (space, tab, newline, and so on)

  • \S Non-whitespace character

  • \d Digit (same as [0-9])

  • \D Non-digit

  • \A Beginning of a string

  • \Z End of a string or before newline at the end

  • \z End of a string

  • \b Word boundary (outside [ ] only)

  • \B Non-word boundary

  • \b Backspace (inside [ ] only)

  • [ ] Any single character of set

  • * Zero or more of the previous subexpression

  • *? Zero or more of the previous subexpression (non-greedy)

  • + One or more of the previous subexpression

  • +? One or more of the previous subexpression (non-greedy)

  • {m,n} M to n instances of the previous subexpression

  • {m,n}? M to n instances of the previous subexpression (non-greedy)

  • ? Zero or one instance of the previous regular expression

  • | Alternatives

  • ( ) Grouping of subexpressions

  • (?# ) Comment

An understanding of regex handling is a powerful tool for the modern programmer. A complete discussion is far beyond the scope of this book. Instead, we refer you to the definitive work Mastering Regular Expressions by Jeffrey Friedl.

An array in Ruby is a very powerful construct; it may contain data of any type or even mixed types. As you'll see in a later section, all arrays are instances of the class Array and therefore have a rich set of methods that can operate on them. An array constant is delimited by brackets. The following are all valid array expressions:

 

 [1, 2, 3] [1, 2, "buckle my shoe"] [1, 2, [3,4], 5] ["alpha", "beta", "gamma", "delta"] 

The second example shows an array containing both integers and strings, the third example shows a nested array, and the fourth shows an array of strings. As in most languages, arrays are "zero indexed;" for instance, in the last array, "gamma" is element number 2. Arrays are dynamic and do not need to have a size specified when they are created.

Because the array of strings is so common (and so inconvenient to type), a special syntax has been set aside for it, similar to what you have seen before:

 

 %w[alpha beta gamma delta] %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) %w/am is are was were be being been/ 

In these examples, the quotes and commas are not needed; only whitespace separates the individual elements. In the case of an element that contains whitespace, of course, this would not work.

An array variable can use brackets to index into the array. The resulting expression can be both examined and assigned to:

 

 val = myarray[0] print stats[j] x[i] = x[i+1] 

Another extremely powerful construct in Ruby is the hash, which is also commonly called an associative array or dictionary. A hash is a set of associations between paired pieces of data; it is typically used as a lookup table or a kind of generalized array in which the index need not be an integer. Each hash is an instance of the class Hash.

A hash constant is typically represented between delimiting braces, with the symbol => separating the individual keys and values. The key can be thought of as an index where the corresponding value is stored. There is no restriction on the types of the keys or the corresponding values. Here are some hashes:

 

 {1=>1, 2=>4, 3=>9, 4=>16, 5=>25, 6=>36} { "cat"=>"cats", "ox"=>"oxen", "bacterium"=>"bacteria"} { "hydrogen"=>1, "helium"=>2, "carbon"=>12} { "odds"=>[1,3,5,7], "evens"=>[2,4,6,8]} {"foo"=>123, [4,5,6]=>"my array", "867-5309"=>"Jenny"} 

A hash variable can have its contents accessed by essentially the same bracket notation that arrays use:

 

 print phone_numbers["Jenny"] plurals["octopus"] = "octopi" 

It should be stressed, however, that both arrays and hashes have many methods associated with them; these methods give them their real usefulness. The next section, covering Ruby OOP, will expand on this a little more.

Operators and Precedence

Now that we have established our most common data types, let's look at Ruby's operators. They are arranged here in order from highest to lowest precedence:

  • Scope ::

  • Indexing []

  • Exponentiation **

  • Unary positive/negative, etc. + - ! ~

  • Multiplication, etc. * / %

  • Addition/subtraction + -

  • Logical shifts, etc. << >>

  • Bitwise and &

  • Bitwise or, xor | ^

  • Comparison > >= < <=

  • Equality, etc. == === <=> != =~ !~

  • Boolean and &&

  • Boolean or ||

  • Range operators .. ...

  • Assignment = (also +=, -=, *=, etc.)

  • Ternary decision ?:

  • Boolean negation not

  • Boolean and, or and or

Some of these operators serve more than one purpose; for example, the operator << is a bitwise left shift but is also an append operator (for arrays, strings, and so on) and a marker for a here-document. Likewise, the plus sign (+) is for addition and for string concatenation. As you'll see later, many of these operators are just shortcuts for method names.

Now we have defined most of the data types and many of the possible operations on them. Before going any further, let's look at an actual sample program.

A Sample Program

In a tutorial, the first program is always "Hello, World!" But in a whirlwind tour like this one, let's start with something slightly more advanced. Here's a small program to convert between Fahrenheit and Celsius temperatures:

 

 print "Please enter a temperature and scale (C or F): " str = gets exit if not str or not str[0] str.chomp! temp, scale = str.split(" ") if temp !~ /-?[0-9]+/   print temp, " is not a valid number.\n"   exit 1 end temp = temp.to_f case scale   when "C", "c"     f = 1.8*temp + 32   when "F", "f"     c = (5.0/9.0)*(temp-32)   else     print "Must specify C or F.\n"     exit 2 end if c != nil then   print "#{ c}  degrees C\n" else   print "#{ f}  degrees F\n" end 

Here are some examples of running this program. These show that the program can convert from Fahrenheit to Celsius and from Celsius to Fahrenheit and that it can handle an invalid scale or an invalid number:

 

 Please enter a temperature and scale (C or F): 98.6 F 37.0 degrees C Please enter a temperature and scale (C or F): 100 C 212.0 degrees F Please enter a temperature and scale (C or F): 92 G Must specify C or F. Please enter a temperature and scale (C or F): junk F junk is not a valid number. 

Now, as for the mechanics of the program, we begin with a print statement, which is actually a call to the predefined function print, to write to standard output. Following this, we call gets (get string from standard input), assigning the value to str.

Note that any apparently "free-standing" function calls such as print and gets are actually methods of various predefined classes or objects. In the same way, chomp is a method that is called with str as a receiver. Method calls in Ruby generally can omit the parentheses; for example, print "foo" is the same as print("foo").

The variable str holds a character string, but there is no reason it could not hold some other type instead. In Ruby, data has types but variables do not.

The special method call exit will terminate the program. On this same line is a control structure called an if modifier. This is like the if statement that exists in most languages, but backwards; it comes after the action, does not permit an else, and does not require closing. As for the condition, we are checking two things: Does str have a value, and is it non-null? In the case of an immediate end-of-file, our first condition will hold; in the case of a newline with no preceding data, the second condition will hold.

The reason these tests work is that a variable that is undefined has a nil value, and nil evaluates to false in Ruby. In fact, nil and false evaluate as false, and everything else evaluates as true. Specifically, the null string "" does not evaluate as false, as it does in some other languages.

The next statement performs a chomp! operation on the string (to remove any trailing newline characters). The exclamation point as a prefix serves as a warning that the operation actually changes the value of its receiver rather than just returning a value. The exclamation point is used in many such instances to remind the programmer that a method has a side effect or is more "dangerous" than its unmarked counterpart. The method chomp, for example, will return the same result but will not modify its receiver.

The next statement is an example of multiple assignment. The split method splits the string into an array of values, using the space as a delimiter. The two assignable entities on the left side will be assigned the respective values resulting on the right side.

The if statement that follows uses a simple regex to determine whether the number is valid; if the string fails to match a pattern consisting of an optional minus sign followed by one or more digits, it is an invalid number and the program exits. Note that the if statement is terminated by the keyword end; although it's not needed here, we could have had an else clause before end. The keyword then is optional; this statement does not use then, but the one below it does. As for the output, recall that the variable temp could also have been embedded in the string (as below).

The to_f method is used to convert the string to a floating-point number. We are actually assigning this floating-point value back to temp, which originally held a string.

The case statement chooses between three alternatives: the case in which the user specifies a C, specifies an F, or uses an invalid scale. In the first two instances, a calculation is done; in the third, we print an error and exit.

Ruby's case statement, by the way, is far more general than the example shown here. There is no limitation on the data types, and the expressions used are all arbitrary and may even be ranges or regular expressions.

There is nothing mysterious about the computation. However, consider the fact that the variables c and f are referenced first inside the branches of the case. There are no declarations as such in Ruby; a variable comes into existence when it is assigned. This means that when we fall through the case statement, only one of these variables will have a value.

We use this to determine after the fact which branch was followed so that we can create a slightly different output in each instance. The comparison of c with nil is effectively a test of whether c has a value. We do this here only to show that it can be done; obviously two different print statements could be used inside the case statement if we wished.

You may have noticed that we've used only "local" variables here. This might be somewhat confusing, because their scope certainly appears to cover the entire program. What's happening here is that the variables are all local to the top level of the program (written as toplevel by some). The variables appear "global" because there are no lower-level contexts in a program this simple; however, if we declared classes and methods, these top-level variables would not be accessible within them.

Looping and Branching

Let's spend some time looking at control structures. We have already seen the simple if statement and the if modifier; there are also corresponding structures based on the keyword unless (which also has an optional else) as well as expression-oriented forms of if and unless. We summarize all these as follows:

 

 if x < 5 then   statement1 end unless x >= 5 then   statement1 end if x < 5 then   statement1 else   statement2 end unless x < 5 then   statement2 else   statement1 end statement1 if y == 3 statement1 unless y != 3 x = if a>0 then b else c end x = unless a<=0 then c else b end 

In this summary, the if and unless forms behave exactly the same. Note that the keyword then may be omitted except in the final (expression-oriented) cases. Note also that the modifier forms cannot have an else clause.

The case statement in Ruby is more powerful than in most languages. This multiway branch can even test for conditions other than equalityfor example, a matched pattern. The test done by the case statement corresponds to the relationship operator (===), which has a behavior that varies from one object to another. Let's look at an example:

 

 case "This is a character string."   when "some value"     print "Branch 1\n"   when "some other value"     print "Branch 2\n"   when  /char/     print "Branch 3\n"   else     print "Branch 4\n" end 

This code will print Branch 3. Why? It first tries to check for equality between the tested expression and one of the strings "some value" or "some other value". This fails, so it proceeds. The third test is for the presence of a pattern within the tested expression; that pattern is there, so the test succeeds and the third print statement is performed. The else clause will always handle the default case in which none of the preceding tests succeeds.

If the tested expression is an integer, the compared value can be an integer range (for example, 3..8). In this case, the expression will be tested for membership in that range. In all instances, the first successful branch will be taken.

As for looping mechanisms, Ruby has a rich set. The while and until control structures are both pretest loops, and both work as expected: One specifies a continuation condition for the loop, and the other specifies a termination condition. They also occur in "modifier" form like if and unless. There is also the loop method of the Kernel module (by default an infinite loop), and iterators (described later) are associated with various classes.

The following examples assume an array called list, defined something like this:

 

 list = %w[alpha bravo charlie delta echo] 

They all step through the array and write out each element:

 

 # Loop 1 (while) i=0 while i < list.size do   print "#{ list[i]}  "   i += 1 end # Loop 2 (until) i=0 until i == list.size do   print "#{ list[i]}  "   i += 1 end # Loop 3 (post-test while) i=0 begin   print "#{ list[i]}  "   i += 1 end while i < list.size # Loop 4 (post-test until) i=0 begin   print "#{ list[i]}  "   i += 1 end until i == list.size # Loop 5 (for) for x in list do   print "#{ x}  " end # Loop 6 ('each' iterator) list.each do |x|   print "#{ x}  " end # Loop 7 ('loop' method) i=0 n=list.size-1 loop do   print "#{ list[i]}  "   i += 1   break if i > n end # Loop 8 ('loop' method) i=0 n=list.size-1 loop do   print "#{ list[i]}  "   i += 1   break unless i <= n end # Loop 9 ('times' iterator) n=list.size n.times do |i|   print "#{ list[i]}  " end # Loop 10 ('upto' iterator) n=list.size-1 0.upto(n) do |i|   print "#{ list[i]}  " end # Loop 11 (for) n=list.size-1 for i in 0..n do   print "#{ list[i]}  " end # Loop 12 ('each_index') list.each_index do |x|   print "#{ list[x]}  " end 

Let's examine these in a little detail. Loops 1 and 2 are the "standard" forms of the while and until loops; they behave essentially the same, but their conditions are negations of each other. Loops 3 and 4 are the same thing in "post-test" versions; the test is performed at the end of the loop rather than at the beginning. Note that the use of begin and end in this context is strictly a kludge or hack; what is really happening is that a begin/end block (used for exception handling) is followed by a while or until modifier. For someone really wanting a post-test loop, however, this is effectively the same.

Loops 5 and 6 are arguably the "proper" ways to write this loop. Note the simplicity of these two compared with the others; there is no explicit initialization and no explicit test or increment. This is because an array "knows" its own size, and the standard iterator each (loop 6) handles such details automatically. Indeed, loop 5 is merely an indirect reference to this same iterator because the for loop will work for any object having the iterator each defined. The for loop is only shorthand for a call to each; such shorthand is frequently called syntax sugar because it offers a more convenient alternative to another syntactic form.

Loops 7 and 8 both make use of the loop construct; as mentioned earlier, loop looks like a keyword introducing a control structure, but it is really a method of the module Kernel, not a control structure at all.

Loops 9 and 10 take advantage of the fact that the array has a numeric index; the times iterator will execute a specified number of times, and the upto iterator will carry its parameter up to the specified value. Neither of these is truly suitable for this instance.

Loop 11 is a for loop that operates specifically on the index values, using a range, and loop 12 likewise uses the each_index iterator to run through the list of array indexes.

In the preceding examples, we have not laid enough emphasis on the "modifier" form of the while and until loops. These are frequently useful, and they have the virtue of being concise. We offer these additional examples, both of which mean the same thing:

 

 perform_task() until finished perform_task() while not finished 

One fact is largely ignored here: Loops do not always run smoothly from beginning to end, in a predictable number of iterations, or ending in a single predictable way. We need ways to control these loops further.

The first of these is the break keyword, which you see in loops 7 and 8. This is used to "break out" of a loop; in the case of nested loops, only the innermost one is halted. This will be intuitive for C programmers.

The keyword retry is used in two contexts: in the context of an iterator and in the context of a begin/end block (exception handling). Within the body of any iterator (or for loop) it will force the iterator to restart, reevaluating any arguments passed to the iterator. Note that it will not work for loops in general (while and until).

The redo keyword is the generalized form of retry for loops. It works for while and until loops just as retry works for iterators.

The next keyword will effectively jump to the end of the innermost loop and resume execution from that point. It works for any loop or iterator.

The iterator is an important concept in Ruby, as you have already seen. What you have not seen is that the language allows user-defined iterators in addition to the predefined ones.

The default iterator for any object is called each. This is significant because it allows the for loop to be used. However, iterators may be given different names and used for varying purposes.

As a crude example, consider this multipurpose iterator, which mimics a post-test loop (like C's do-while or Pascal's repeat-until):

 

 def repeat(condition)   yield   retry if not condition end 

In this example, the keyword yield is used to call the block that is specified when the iterator is called in this way:

 

 j=0 repeat (j<10) do {  j+=1; print j,"\n"} 

It is also possible to pass parameters via yield, which will be substituted into the block's parameter list (between vertical bars). As a somewhat contrived example, the following iterator does nothing but generate integers from 1 to 10, and the call of the iterator generates the first 10 cubes:

 

 def my_sequence   for i in 1..10 do     yield i   end end my_sequence { |x| print x**3, "\n"} 

Note that do and end may be substituted for the braces that delimit a block. There are differences, but they are fairly subtle.

Exceptions

Like many other modern programming languages, Ruby supports exceptions. An exception is a means of handling errors that has significant advantages over older methods. Return codes are avoidable, as is the "spaghetti logic" that results from checking them. Also, the code that detects the error can be distinguished from the code that knows how to handle the error (because these are often separate anyway).

The raise statement will raise an exception. Note that raise is not a reserved word but rather a method of the module Kernel. (Its alias is named fail.) Here are examples:

 

 raise                                           # Example 1 raise "Some error message."                     # Example 2 raise ArgumentError                             # Example 3 raise ArgumentError, "Invalid data."            # Example 4 raise ArgumentError.new("Invalid data.")        # Example 5 raise ArgumentError, "Invalid data.", caller[0] # Example 6 

In example 1, the last exception encountered is re-raised. In example 2, a RuntimeError (the default error) is created using the message "Some error message." In example 3, an ArgumentError is raised; in example 4, this same error is raised with the message "Invalid data." Example 5 behaves exactly the same as example 4. Finally, example 6 adds traceback information of the form "filename:line" or "filename:line:in `method'" (as stored in the array returned by the caller method or stored in the $a special variable).

Now, how do we handle exceptions in Ruby? The begin-end block is used for this purpose. The simplest form is a begin-end block with nothing but our code inside:

 

 begin  # No real purpose.   # ... end 

This, however, is of no value in catching errors. The block, however, may have one or more rescue clauses in it. If an error occurs at any point in the code, between begin and rescue, control will be passed immediately to the appropriate rescue clause. Here's an example:

 

 begin   x = Math.sqrt(y/z)   # ... rescue ArgumentError   print "Error taking square root.\n" rescue ZeroDivisionError   print "Attempted division by zero.\n" end 

Essentially the same thing can be accomplished by this fragment:

 

 begin   x = Math.sqrt(y/z)   # ... rescue => err   print err, "\n" end 

Here, the variable err is used to store the value of the exception; printing it causes it to be translated to some meaningful character string. Note that because the error type is not specified, the rescue clause will catch every kind of error. The notation rescue => variable can be used with or without an error type before the => symbol.

In the event that error types are specified, it may be that an exception does not match any of these types. For that situation, we are allowed to use an else clause after all the rescue clauses:

 

 begin   # Error-prone code... rescue Type1   # ... rescue Type2   # ... else   # Other exceptions... end 

In many cases, we will want to do some kind of recovery. In that event, the keyword retry (within the body of a rescue clause) will restart the begin block and try those operations again:

 

 begin   # Error-prone code... rescue   # Attempt recovery...   retry end 

Finally, it is sometimes necessary to write code that "cleans up" after a begin-end block. In the event this is necessary, an ensure clause can be specified:

 

 begin   # Error-prone code... rescue   # Handle exceptions ensure   # This code is always executed end 

The code in an ensure clause is always executed before the begin-end block exits. This happens regardless of whether an exception occurred.

There are two other ways in which exceptions may be caught. First of all, there is a modifier form of the rescue clause:

 

 x = a/b  rescue print "Division by zero!\n" 

In addition, the body of a method definition is an implicit begin-end block; the begin is omitted, and the entire body of the method is subject to exception handling, ending with the end of the method:

 

 def some_method     # Code... rescue     # Recovery... end 

This sums up the discussion of exception handling as well as the discussion of fundamental syntax and semantics.

Numerous aspects of Ruby have not been discussed here. The next two sections are devoted to the more advanced features of the language, and the final section is mostly a collection of Ruby lore that will help the intermediate programmer learn to "think in Ruby."


   

 

 



The Ruby Way
The Ruby Way, Second Edition: Solutions and Techniques in Ruby Programming (2nd Edition)
ISBN: 0672328844
EAN: 2147483647
Year: 2000
Pages: 119
Authors: Hal Fulton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net