More Advanced Techniques

 
   

Ruby Way
By Hal Fulton
Slots : 1.0
Table of Contents
 


Not everything in Ruby OOP is straightforward. Some techniques are more complex than others, and some are rarely used. The dividing line will be different for each programmer. We've tried to put items in this part of the chapter that were slightly more involved or slightly more rare in terms of usage.

From time to time, you might ask yourself whether it's possible to do some task or other in Ruby. The short answer is that Ruby is a rich dynamic OOP language with a good set of reasonably orthogonal features; and if you want to do something that you're used to in another language, you can probably do it in Ruby.

As a matter of fact, all Turing-complete languages are pretty much the same from a theoretical standpoint. The whole field of language design is the search for a meaningful, convenient notation. Those of you who doubt the importance of a convenient notation should try writing a LISP interpreter in COBOL or doing long division with Roman numerals.

Of course, we won't say that every language task is elegant or natural in Ruby. Someone would quickly prove us wrong if we made that assertion.

This section also touches on the use of Ruby in various advanced programming styles such as functional programming and aspect-oriented programming. We don't claim expertise in these areas; we are only reporting what other people are saying. Take it all with a grain of salt.

Sending an Explicit Message to an Object

In a static language, you take it for granted that when you call a function, that function name is hard-coded into the program; it is part of the program source. In a dynamic language, we have more flexibility than that.

Every time you invoke a method, you're sending a message to an object. Most of the time, these messages are hard-coded as in a static language, but they need not always be. We can write code that determines at runtime which method to call. The send method will allow us to use a Symbol to represent a method name.

For an example, suppose that we had an array of objects we wanted to sort, and we wanted to be able to use different fields as sort keys. That's not a problem; we can easily write customized sort blocks. But suppose that we wanted to be a little more elegant and write only a single routine that could sort based on whatever key we specified. Listing 5.9 shows an example.

Listing 5.9 Sorting by Any Key
 class Person   attr_reader :name,     :age,     :height   def initialize(name, age, height)     @name, @age, @height = name, age, height   end    def inspect      "#@name #@age #@height"    end end class Array   def sort_by(sym)     self.sort { |x,y| x.send(sym) <=> y.send(sym) }   end end people = [] people << Person.new("Hansel", 35, 69) people << Person.new("Gretel", 32, 64) people << Person.new("Ted", 36, 68) people << Person.new("Alice", 33, 63) p1 = people.sort_by(:name) p2 = people.sort_by(:age) p3 = people.sort_by(:height) p p1   # [Alice 33 63, Gretel 32 64, Hansel 35 69, Ted 36 68] p p2   # [Gretel 32 64, Alice 33 63, Hansel 35 69, Ted 36 68] p p3   # [Alice 33 63, Gretel 32 64, Ted 36 68, Hansel 35 69] 

Of course, if you really want efficient sorting, this example is incomplete. But it illustrates the example of calling a method whose identity isn't known before runtime.

We'll also mention the alias __send__, which does exactly the same thing. It is given this peculiar name, of course, because send is a name that might be used (purposely or accidentally) as a user-defined method name.

Specializing an Individual Object

I'm a Solipsist, and I must say I'm surprised there aren't more of us.

Letter received by Bertrand Russell

In most object-oriented languages, all objects of a particular class share the same behavior. The class acts as a template, producing an object with the same interface each time the constructor is called.

Although Ruby acts the same way, that isn't the end of the story. When you have a Ruby object, you can change its behavior on-the-fly. Effectively, you're giving that object a private, anonymous subclass: All the methods of the original class are available, but you've added additional behavior for just that object. Because this behavior is private to the associated object, it can only occur once. A thing occurring only once is called a singleton, as in singleton methods and singleton classes.

The word singleton can be confusing because it is also used in a different sense as the name of a well-known design pattern for a class that can only be instantiated once. For this usage, refer to the singleton.rb library.

Here we see a pair of objects, both of which are strings. For the second one, we will add a method upcase that will override the existing method of that name.

 

 a = "hello" b = "goodbye" def b.upcase      # create single method   gsub(/(.)(.)/) {  $1.upcase + $2 } end puts a.upcase   # HELLO puts b.upcase   # GoOdBye 

Adding a singleton method to an object creates a singleton class for that object if one doesn't already exist. This singleton class's parent will be the object's original class. (This could be considered an anonymous subclass of the original class.) If you want to add multiple methods to an object, you can create the singleton class directly.

 

 b = "goodbye" class << b   def upcase      # create single method     gsub(/(.)(.)/) {  $1.upcase + $2 }   end   def upcase!     gsub!(/(.)(.)/) {  $1.upcase + $2 }   end end puts b.upcase  # GoOdBye puts b         # goodbye b.upcase! puts b         # GoOdBye 

As an aside, we'll note that the more primitive objects (such as a Fixnum) cannot have singleton methods added. This is because an object of this nature is stored as an immediate value rather than as an object reference. However, we expect this functionality to be added in a future revision of Ruby.

If you read some of the library code, you're bound to come across an idiomatic use of singleton classes. Within class definitions, you might see something like this:

 

 class SomeClass   # Stuff...   class << self     # more stuff...   end   # ... and so on. end 

Within the body of a class definition, self is the class you're defining, so creating a singleton based on it modifies the class's class. At the simplest level, this means that instance methods in the singleton class are class methods externally.

 

 class TheClass   class << self     def hello       puts "hi"     end   end end # invoke a class method TheClass.hello            # hi 

Another common use of this technique is to define class-level helper functions, which we can then access in the rest of the class definition. As an example, we want to define several accessor functions that always convert their results to a string. We could do this by coding each individually. A neater way might be to define a class-level function accessor_string that generates these functions for us (as shown in Listing 5.10).

Listing 5.10 A Class-level Method accessor_string
 class MyClass   class << self     def accessor_string(*names)       names.each do |name|         class_eval <<-EOD           def #{ name}             @#{ name} .to_s            end          EOD        end      end    end    def initialize      @a = [ 1, 2, 3 ]      @b = Time.now    end    accessor_string :a, :b  end  o = MyClass.new  puts o.a           # 123  puts o.b           # Mon Apr 30 23:12:15 CDT 2001 

More imaginative examples are left up to you.

The extend method will mix a module into an object. The instance methods from the module become instance methods for the object. Let's look at Listing 5.11.

Listing 5.11 Using extend
 module Quantifier def any?   self.each {  |x| return true if yield x }   false end def all?   self.each {  |x| return false if not yield x }    true end end list = [1, 2, 3, 4, 5] list.extend(Quantifier) flag1 = list.any? { |x| x > 5 }         # false flag2 = list.any? { |x| x >= 5 }        # true flag3 = list.all? { |x| x <= 10 }       # true flag4 = list.all? { |x| x % 2 == 0 }    # false 

In this example, the any? and all? methods are mixed into the list array.

Nesting Classes and Modules

We'll point out that it's possible to nest classes and modules arbitrarily. The programmer new to Ruby might not know this.

Mostly this is for namespace management. Note that the File class has a Stat class embedded inside it. This helps to encapsulate the Stat class inside a class of related functionality, and also allows for a future class named Stat, which won't conflict with that one (perhaps a statistics class, for instance).

The Struct::Tms class is a similar example. Any new Struct is placed in this namespace so as not to pollute the one above it, and Tms is really just another Struct.

It's also conceivable that you might want to create a nested class simply because the outside world doesn't need that class or shouldn't access it. In other words, you can create classes that are subject to the principle of data hiding just as the instance variables and methods are subject to the same principle at a lower level.

 

 class BugTrackingSystem   class Bug     #...   end   #... end # Nothing out here knows about Bug. 

You can nest a class within a module, a module within a class, and so on. If you find interesting and creative uses for this technique, let us all know about it.

Creating Parametric Classes

Learn the rules; then break them.

Basho

Suppose that we wanted to create multiple classes that differed only in the initial values of the class-level variables. Recall that a class variable is typically initialized as a part of the class definition.

 

 class Terran   @@home_planet = "Earth"   def Terran.home_planet     @@home_planet   end   def Terran.home_planet=(x)     @@home_planet = x   end   #... end 

That is all fine, but suppose that we had a number of similar classes to define? The novice will think, "Ah, I'll just define a superclass." (See Listing 5.12.)

Listing 5.12 Parametric Classes #1
 class IntelligentLife   # Wrong way to do this!   @@home_planet = nil   def IntelligentLife.home_planet     @@home_planet   end   def IntelligentLife.home_planet=(x)      @@home_planet = x    end #...  end  class Terran < IntelligentLife    @@home_planet = "Earth"    #...  end  class Martian < IntelligentLife    @@home_planet = "Mars"    #...  end 

But this won't work. If we call Terran.home_planet, we expect a result of "Earth"but we get "Mars"!

Why would this happen? The answer is that class variables aren't truly class variables; they belong not to the class, but to the entire inheritance hierarchy. The class variables aren't copied from the parent class, but are shared with the parent (and thus with the sibling classes).

We could eliminate the definition of the class variable in the base class; but then the class methods we define would no longer work!

We could fix this by moving these definitions to the child classes, but now we've defeated our whole purpose. We're declaring separate classes without any parameterization.

We'll offer a different solution. We'll defer the evaluation of the class variable until runtime by using the class_eval method. Listing 5.13 shows a complete solution.

Listing 5.13 Parametric Classes #2
 class IntelligentLife   def IntelligentLife.home_planet     class_eval("@@home_planet")   end   def IntelligentLife.home_planet=(x)     class_eval("@@home_planet = #{ x} ")   end    #... end class Terran < IntelligentLife   @@home_planet = "Earth"   #... end class Martian < IntelligentLife   @@home_planet = "Mars"   #... end puts Terran.home_planet            # Earth puts Martian.home_planet           # Mars 

It goes without saying that inheritance still operates normally here. Any instance methods or instance variables defined within IntelligentLife will be inherited by Terran and Martian just as you would expect.

As a minor variation on this theme, we present a slightly different way of doing the same thing (see Listing 5.14). Here we have overridden the Class.new method to create a parametric class for us; we inherit from the specified base class and then do a class_eval of the block passed in.

Listing 5.14 Parametric Classes #3
 class Class   def initialize(klass, &block)     block = Proc.new     class_eval(&block)   end end class IntelligentLife   def IntelligentLife.home_planet     class_eval("@@home_planet")   end   def IntelligentLife.home_planet=(x)     class_eval("@@home_planet = #{ x} ")   end   #... end Terran = Class.new(IntelligentLife) do    @@home_planet = "Earth" end Martian = Class.new(IntelligentLife) do    @@home_planet = "Mars" end 

This technique resembles the fancy constructor of the section "More Elaborate Constructors." The principle is the same, but we are working at the class level rather than the instance level. (Of course, Class is an object, so we can still regard it as the instance level if we want.)

We should mention that there are other ways of doing this. Use your creativity.

Using Continuations to Implement a Generator

One of the more abstruse features of Ruby is the continuation. This is a structured way of handling a nonlocal jump and return; a continuation object stores a return address and an execution context. It is somewhat analogous to the setjmp/longjmp feature in C, but it stores more context.

The Kernel method callcc takes a block and returns an object of the Continuation class. The object returned is also passed into the block as a parameter, just to keep things confusing.

The only method of Continuation is call, which causes a nonlocal return to the end of the callcc block. The callcc can be terminated either by falling through the block or by calling the call method.

There is a known shortage of examples of how to use continuations. The best one we have seen comes from Jim Weirich, who implemented a generator as a result of his discussion with another Ruby programmer, Hugh Sasse.

A generator is made possible by suspend in Icon (also found in Prolog), which allows a function to resume execution just after the last place it returned a value. Hugh describes it as similar to an inside-out yield.

Listing 5.15, then, is Jim's implementation of a generator that generates Fibonacci numbers one after another. Continuations are used to preserve the call state from one invocation to the next.

Listing 5.15 Fibonacci Generator
 class Generator   def initialize     do_generation   end   def next     callcc {  |here|       @main_context = here;        @generator_context.call      }    end    private    def do_generation      callcc {  |context|        @generator_context = context;        return      }      generating_loop    end    def generate(value)      callcc {  |context|        @generator_context = context;        @main_context.call(value)      }    end end # Subclass this and define a generating_loop class FibGenerator < Generator   def generating_loop     generate(1)     a, b = 1, 1     loop do       generate(b)       a, b = b, a+b     end   end end  # Now instantiate the class...  fib = FibGenerator.new puts fib.next            # 1 puts fib.next            # 1 puts fib.next            # 2 puts fib.next            # 3 puts fib.next            # 5 puts fib.next            # 8 puts fib.next            # 13 # And so on... 

We can't help but feel that there are practical applications for this idea. If you think of some, share them with us all.

Storing Code as Objects

Not surprisingly, Ruby gives you several alternatives when it comes to storing a chunk of code in the form of an object. In this section, we'll take a look at Proc objects, Method objects, and UnboundMethod objects.

The built-in class Proc is used to wrap Ruby blocks in an object. Proc objects, like blocks, are closures, and therefore carry around the context in which they were defined.

 

 p = Proc.new {  |a| puts "Param is #{ a} " } p.call(99)         # Param is 99 

Proc objects are also created automatically by Ruby when a method defined with a trailing & parameter is called with a block.

 

 def take_block(a, &block)   puts block.type   a.times { |i| block[i, i*i] } end take_block(3) {  |n,s| puts "#{ n}  squared is #{ s} " } 

This example also shows the use of braces ({}) as an alias for the call method. The output is shown here:

 

 Proc 0 squared is 0 1 squared is 1 2 squared is 4 

If you have a Proc object, you can pass it to a method that's expecting a block, preceding its name with an &, as shown here:

 

 p = proc {  |n| print n, "... " } (1..3).each(&p)                    # 1... 2... 3... 

Ruby also lets you turn a method into an object. Historically, this is done using Object#method, which creates a Method object as a closure in a particular object.

 

 str = "cat" meth = str.method(:length) a = meth.call               #  3  (length of "cat") str << "erpillar" b = meth.call               # 11  (length of "caterpillar") str = "dog" # Note the next call! The variable str refers to a new object # ("dog") now, but meth is still bound to the old object. c = meth.call               # 11  (length of "caterpillar") 

As of Ruby 1.6.2, you can also use Module#instance_method to create UnboundMethod objects. These represent a method that is associated with a class, rather than one particular object. Before calling an UnboundMethod object, you must first bind it to a particular object. This act of binding produces a Method object, which you call normally.

 

 umeth = String.instance_method(:length) m1 = umeth.bind("cat") m1.call                         # 3 m2 = umeth.bind("caterpillar") m2.call                         # 11 

This explicit binding makes the UnboundMethod object a little more intuitive than Method.

Automatically Defining Class-level Readers and Writers

You have seen the methods attr_reader, attr_writer, and attr_accessor, which make it a little easier to define readers and writers (getters and setters) for instance attributes. But what about class-level attributes?

Ruby has no similar facility for creating these automatically. But we can make our own facility by adding to the class Class. In Listing 5.16 we name them similarly, only prefixing the names with a c for class.

Listing 5.16 A Shorthand for Creating Class Attributes
 class Class   def cattr_reader(*syms)     syms.each do |sym|       class_eval <<-EOS         if ! defined? @@#{ sym.id2name}           @@#{ sym.id2name}  = nil         end         def self.#{ sym.id2name}            @@#{ sym}          end        EOS      end    end    def cattr_writer(*syms)      syms.each do |sym|        class_eval <<-EOS          if ! defined? @@#{ sym.id2name}            @@#{ sym.id2name}  = nil          end          def self.#{ sym.id2name} =(obj)            @@#{ sym.id2name}  = obj          end        EOS      end    end    def cattr_accessor(*syms)      cattr_reader(*syms)      cattr_writer(*syms)    end end class MyClass    @@alpha = 123                # Initialize @@alpha    cattr_reader :alpha          # MyClass.alpha()    cattr_writer :beta           # MyClass.beta=()    cattr_accessor :gamma        # MyClass.gamma() and                                 #   MyClass.gamma=()    def MyClass.look      puts "#@@alpha, #@@beta, #@@gamma"    end    #... end puts MyClass.alpha             # 123 MyClass.beta = 456 MyClass.gamma = 789 puts MyClass.gamma             # 789 MyClass.look                   # 123, 456, 789 

Most classes are no good without instance level data. We've only omitted it from Listing 5.16 for clarity.


   

 

 



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

Similar book on Amazon

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