Missing Methods and Missing Constants

The method_missing method is a key part of Ruby’s method lookup algorithm (see Method Lookup) and provides a powerful way to catch and handle arbitrary invocations on an object. The const_missing method of Module performs a similar function for the constant lookup algorithm and allows us to compute or lazily initialize constants on the fly. The examples that follow demonstrate both of these methods.

Unicode Codepoint Constants with const_missing

Example 8-3 defines a Unicode module that appears to define a constant (a UTF-8 encoded string) for every Unicode codepoint from U+0000 to U+10FFFF. The only practical way to support this many constants is to use the const_missing method. The code makes the assumption that if a constant is referenced once, it is likely to be used again, so the const_missing method calls Module.const_set to define a real constant to refer to each value it computes.

Example 8-3. Unicode codepoint constants with const_missing

# This module provides constants that define the UTF-8 strings for
# all Unicode codepoints. It uses const_missing to define them lazily.
# Examples:
#   copyright = Unicode::U00A9
#   euro = Unicode::U20AC
#   infinity = Unicode::U221E
module Unicode
  # This method allows us to define Unicode codepoint constants lazily.
  def self.const_missing(name)  # Undefined constant passed as a symbol
    # Check that the constant name is of the right form.
    # Capital U followed by a hex number between 0000 and 10FFFF.
    if name.to_s =~ /^U([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/
      # $1 is the matched hexadecimal number. Convert to an integer.
      codepoint = $1.to_i(16)
      # Convert the number to a UTF-8 string with the magic of Array.pack.
      utf8 = [codepoint].pack("U")
      # Make the UTF-8 string immutable.
      utf8.freeze
      # Define a real constant for faster lookup next time, and return
      # the UTF-8 text for this time.
      const_set(name, utf8)
    else 
      # Raise an error for constants of the wrong form.
      raise NameError, "Uninitialized constant: Unicode::#{name}"
    end
  end
end

Tracing Method Invocations with method_missing

Earlier in this chapter, we demonstrated an extension to the Hash class using method_missing. Now, in Example 8-4, we demonstrate the use of method_missing to delegate arbitrary calls on one object to another object. In this example, we do this in order to output tracing messages for the object.

Example 8-4 defines an Object.trace instance method and a TracedObject class. The trace method returns an instance of TracedObject that uses method_missingto catch invocations, trace them, and delegate them to the object being traced. You might use it like this:

a = [1,2,3].trace("a")
a.reverse
puts a[2]
puts a.fetch(3)

This produces the following tracing output:

Invoking: a.reverse() at trace1.rb:66
Returning: [3, 2, 1] from a.reverse to trace1.rb:66
Invoking: a.fetch(3) at trace1.rb:67
Raising: IndexError:index 3 out of array from a.fetch

Notice that in addition to demonstrating method_missing, Example 8-4 also demonstrates Module.instance_methods, Module.undef_method, and Kernel.caller.

Example 8-4. Tracing method invocations with method_missing

# Call the trace method of any object to obtain a new object that
# behaves just like the original, but which traces all method calls
# on that object. If tracing more than one object, specify a name to
# appear in the output. By default, messages will be sent to STDERR, 
# but you can specify any stream (or any object that accepts strings
# as arguments to <<).
class Object
  def trace(name="", stream=STDERR)
    # Return a TracedObject that traces and delegates everything else to us.
    TracedObject.new(self, name, stream)
  end
end

# This class uses method_missing to trace method invocations and
# then delegate them to some other object. It deletes most of its own
# instance methods so that they don't get in the way of method_missing.
# Note that only methods invoked through the TracedObject will be traced.
# If the delegate object calls methods on itself, those invocations
# will not be traced.
class TracedObject
  # Undefine all of our noncritical public instance methods.
  # Note the use of Module.instance_methods and Module.undef_method.
  instance_methods.each do |m|
    m = m.to_sym  # Ruby 1.8 returns strings, instead of symbols
    next if m == :object_id || m == :__id__ || m == :__send__
    undef_method m
  end

  # Initialize this TracedObject instance.
  def initialize(o, name, stream)
    @o = o            # The object we delegate to
    @n = name         # The object name to appear in tracing messages
    @trace = stream   # Where those tracing messages are sent
  end

  # This is the key method of TracedObject. It is invoked for just
  # about any method invocation on a TracedObject.
  def method_missing(*args, &block)
    m = args.shift         # First arg is the name of the method
    begin
      # Trace the invocation of the method.
      arglist = args.map {|a| a.inspect}.join(', ')
      @trace << "Invoking: #{@n}.#{m}(#{arglist}) at #{caller[0]}
"
      # Invoke the method on our delegate object and get the return value.
      r = @o.send m, *args, &block
      # Trace a normal return of the method.
      @trace << "Returning: #{r.inspect} from #{@n}.#{m} to #{caller[0]}
"
      # Return whatever value the delegate object returned.
      r
    rescue Exception => e
      # Trace an abnormal return from the method.
      @trace << "Raising: #{e.class}:#{e} from #{@n}.#{m}
"
      # And re-raise whatever exception the delegate object raised.
      raise
    end
  end

  # Return the object we delegate to.
  def __delegate
    @o
  end
end

Synchronized Objects by Delegation

In Example 8-2, we saw a global method synchronized, which accepts an object and executes a block under the protection of the Mutex associated with that object. Most of the example consisted of the implementation of the Object.mutex method. The synchronized method was trivial:

def synchronized(o)
  o.mutex.synchronize { yield }
end

Example 8-5 modifies this method so that, when invoked without a block, it returns a SynchronizedObject wrapper around the object. SynchronizedObject is a delegating wrapper class based on method_missing. It is much like the TracedObject class of Example 8-4, but it is written as a subclass of Ruby 1.9’s BasicObject, so there is no need to explicitly delete the instance methods of Object. Note that the code in this example does not stand alone; it requires the Object.mutex method defined earlier.

Example 8-5. Synchronizing methods with method_missing

def synchronized(o)
  if block_given?
    o.mutex.synchronize { yield }
  else
    SynchronizedObject.new(o)
  end
end

# A delegating wrapper class using method_missing for thread safety
# Instead of extending Object and deleting our methods we just extend
# BasicObject, which is defined in Ruby 1.9. BasicObject does not 
# inherit from Object or Kernel, so the methods of a BasicObject cannot
# invoke any top-level methods: they are just not there.
class SynchronizedObject  < BasicObject
  def initialize(o); @delegate = o;  end
  def __delegate; @delegate; end

  def method_missing(*args, &block)
    @delegate.mutex.synchronize {
      @delegate.send *args, &block
    }
  end
end
..................Content has been hidden....................

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