Dynamic Aspects of Ruby

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


Ruby is a very dynamic language in the sense that objects and classes may be altered at runtime. It has the ability to construct and evaluate pieces of code in the course of executing the existing statically coded program. It has a sophisticated reflection API that makes it very "self-aware"; this enables the easy creation of debuggers, IDEs, and similar tools, and it also makes certain advanced coding techniques possible.

This is perhaps the most difficult area a programmer will encounter in learning Ruby. Here, we briefly examine some of the implications of Ruby's dynamic nature.

Coding at Runtime

We have already mentioned load and require earlier. However, it is important to realize that these are not built-in statements or control structures or anything of that nature; they are actual methods. Therefore, it is possible to call them with variables or expressions as parameters or to call them conditionally. Contrast with this the #include directive in C and C++, which is evaluated and acted on at compile-time.

Ruby also enables the program to get access to the names of its own variables. Here's a variable called foobar assigned the value 3; following that assignment, the print method prints out not only the value but the name of the variable:

 

 foobar = 3 print "The value is ", foobar, "\n" print "The variable name is ", :foobar.id2name, "\n" 

Of course, this contrived example is not truly useful; the point is that the user's code can retrieve and manipulate internal names at will. Similar but much more sophisticated operations can be done with the reflection API, as you'll see in the next section.

Code can be constructed piecemeal and evaluated. As another contrived example, consider this calculate method and the code calling it:

 

 def calculate(op1, operator, op2)   string = op1.to_s + operator + op2.to_s   # operator is assumed to be a string; make one big   # string of it and the two operands   eval(string)   # Evaluate and return a value end $alpha = 25 $beta = 12 puts calculate(2, "+", 2)          # Prints 4 puts calculate(5, "*", "$alpha")   # Prints 125 puts calculate("$beta", "**", 3)   # Prints 1728 

As an even more extreme example, the following code will prompt the user for a method name and a single line of code; then it will actually define the method and call it:

 

 puts "Method name: " meth_name = gets.chomp puts "Line of code: " line = gets.chomp # Build a string string = %[def #{ meth_name} \n #{ line} \n end] eval(string)       # Define the method eval(meth_name)    # Call the method 

Frequently, programmers wish to code for different platforms or circumstance and still maintain only a single code base. In such a case, a C programmer would use #ifdef directives; in Ruby, however, definitions are executed. There is no "compile time," and everything is dynamic rather than static. Therefore, if we want to make some kind of decision like this, we can simply evaluate a flag at runtime:

 

 if platform == Windows   action1 elsif platform == Linux   action2 else   default_action end 

Of course, there is a small runtime penalty for coding in this way, because the flag may be tested many times in the course of execution. However, the next example does essentially the same thing, enclosing the platform-dependent code in a method whose name is the same across all platforms:

 

 if platform == Windows   def my_action     action1   end elsif platform == Linux   def my_action     action2   end else   def my_action     default_action   end end 

In this way, the same result is achieved, but the flag is only evaluated once. When the user's code calls my_action, it will already have been defined appropriately.

Reflection

Languages such as Smalltalk, LISP, and Java implement the notion of a reflective programming languageone in which the active environment can query the objects that define it as well as extend or modify them at runtime.

Ruby allows reflection quite extensively but does not go as far as Smalltalk, which even represents control structures as objects. Ruby control structures and blocks are not objects (a Proc object can be used to "objectify" a block, but control structures are never objects).

The keyword defined (with an appended question mark) may be used to determine whether an identifier name is in use, as shown here:

 

 if defined? some_var   print "some_var = #{ some_var} \n" else   print "The variable some_var is not known.\n" end 

In most if not all cases, this is equivalent to comparing the variable to nil.

In a similar way, the method respond_to? determines whether an object can respond to the specified method call (that is, whether that method is defined for that object). The respond_to? method is defined in class Object.

Ruby supports runtime type information in a radical way. The type (or class) of an object can be determined at runtime using the method type (defined in Object). Similarly, is_a? will tell whether an object is of a certain class (including the superclasses), and kind_of? is the alias. Here's an example:

 

 print "abc".type              # Prints String print 345.type                # Prints Fixnum rover = Dog.new print rover.type              # Prints Dog if rover.is_a? Dog   print "Of course he is.\n" end if rover.kind_of? Dog   print "Yes, still a dog.\n" end if rover.is_a? Animal   print "Yes, he's an animal, too.\n" end 

It is possible to retrieve an exhaustive list of all the methods that can be invoked for a given object; this is done by using the methods method, defined in Object. There are also variations such as private_instance_methods, public_instance_methods, and so on.

In a similar way, you can determine the class variables and instance variables associated with an object. By the very nature of OOP, the lists of methods and variables include the entities defined not only in the object's class but in its superclasses. The Module class has a method called constants that's used to list all the constants defined.

The class Module has a method ancestors that will return a list of modules that are included in the given module. This list is self-inclusive; Mod.ancestors will always have at least Mod in the list. The class Object has a method called superclass that returns the superclass of the object or returns nil. Because Object itself is the only object without a superclass, it is the only case in which nil will be returned.

The ObjectSpace module is used to access any and all "living" objects. The method _idtoref can be used to convert an object ID to an object reference; it can be considered the inverse of the colon notation. ObjectSpace also has an iterator called each_object that will iterate over all the objects currently in existence, including many that you will not otherwise explicitly know about. (Remember that certain small immutable objects, such as objects of class Fixnum, NilClass, TrueClass, and FalseClass are not kept on the stack for optimization reasons.)

Missing Methods

When a method is invoked (myobject.mymethod), Ruby first searches for the named method according to this search order:

  1. Singleton methods in the receiver myobject.

  2. Methods defined in myobject's class.

  3. Methods defined among myobject's ancestors.

If the method mymethod is not found, Ruby searches for a default method called method_missing. If this method is defined, it is passed the name of the missing method (as a symbol) and all the parameters that were passed to the nonexistent mymethod.

Garbage Collection

Managing memory on a low level is hard and error prone, especially in a dynamic environment such as Ruby. Having a garbage-collection facility is a very significant advantage. In languages such as C++, memory allocation and deallocation are handled by the programmer; in more recent languages such as Java, memory is reclaimed (when objects go out of scope) by a garbage collector.

Memory management done by the programmer is the source of two of the most common kinds of bugs. If an object is freed while still being referenced, a later access may find the memory in an inconsistent state. These so-called "dangling pointers" are difficult to track down because they often cause errors in code that is far removed from the offending statement. A related bug is a "memory leak," caused when an object is not freed even though there are no references to it. Programs with this bug typically use up more and more memory until they crash; this kind of error is also difficult to find. Ruby uses a GC facility that tracks down unused objects and reclaims the storage that was allocated to them. For those who care about such things, Ruby's GC is done using a "mark and sweep" algorithm rather than reference counting (which frequently has difficulties with recursive structures).

Certain performance penalties may be associated with garbage collection. There are some limited controls in the GC module so that the programmer can tailor garbage collection to the needs of the individual program.


   

 

 



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