Introduction
A crucial component of Ruby's method lookup process, the method_missing method offers a powerful way of detecting and handling arbitrary invocations on an object. Similar work is done for the constant lookup technique by the const_missing method of Module, enabling on-the-fly computation or lazy initialization of constants. Both of these techniques are illustrated in the following blog.
Let's dive into the article to get information about missing methods and constants in Ruby.
Unicode Codepoint Constants with const_missing
The example that follows defines a Unicode module that seems to represent a constant (a UTF-8 encoded text) for every Unicode codepoint between U+0000 and U+10FFFF. The const_missing function is the only feasible technique to support this many constants. The const_missing method invokes Module.const_set to define an actual constant to refer to each value it computes since the code assumes that if a constant is referenced once, it will likely be used again.
Program:
# For all Unicode codepoints, this module offers constants that define the #UTF-8 strings
. It hastily defines them using const_missing.
module
Unicode
#
Using this technique, we can define Unicode codepoint constants on the fly.
def self.const_missing(name) # Undefined constant passed as symbol
# Verify the correct form of the constant name
.
# Hexadecimal value between 0000 and 10FFFF, then a capital U.
if name.to_s =~ /^U([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/
#The matched hexadecimal value is $1. To an integer
, convert.
codepoint = $1.to_i(16)
# Convert number to a UTF-8 string with magic of Array.pack.
utf8 = [codepoint].pack("U")
# Make the string immutable in UTF-8.
utf8.freeze
# Define real constant for faster lookup next time,
# and return
the UTF-8 text this time. const_set(name, utf8)
else
# Raise an error for constants of wrong form. raise NameError, "Uninitialized constant: Unicode::#{name}"
end
end
end
Tracing Method Invocations with method_missing
We used method_missing earlier in this chapter to show how the Hash class might be extended. In the following example, we show how to delegate arbitrary calls on one object to another by using method_missing. In this illustration, we carry out this action to produce tracing messages for the object.
A TracedObject class and the object.trace instance method is defined in Example. The return value of the trace method is an instance of the TracedObject class that utilizes method_missing to track down and delegate invoked methods to the object being traced. This is one way to employ it:
a = [1,2,3].trace("a")
a.reverse
puts a[2]
puts a.fetch(3)
This produces the following 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
Program:-
# Call any object's trace function to create a duplicate object that
mimics
#the original while tracking all method calls
made to it. If tracing multiple
# objects, give each one a name
that will appear in the output. Messages
# are often sent to STDERR,
by default, but you can choose any stream (or object)
#that accepts strings
as arguments to.
class Object
def trace(name="", stream=STDERR)
# Return TracedObject that traces and delegates everything. TracedObject.new(self, name, stream)
end
end
# This class uses the method missing function to track method
calls
#before delegating them to another object.
To prevent getting in the way
#of method missing, it deletes the
majority of its instance methods.
# Take note that only calls made via the TracedObject will be trac
ked.
# These invocations
# will not be trac
ked if the delegate object uses its own methods.
class TracedObject
# 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
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]}\n"
# 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]}\n" # 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}\n"
# 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
A global method receives an object and performs a block while being protected by the Mutex connected to that object, as demonstrated in the following example. The object.mutex method's implementation took up most of the example. The synchronized approach was simple:
def synchronized(o)
o.mutex.synchronize { yield }
end
Example1: changes this function by returning a SynchronizedObject wrapper around the object when it is called without a block. A delegating wrapper class based on method_missing is called SynchronizedObject. It is quite similar to the TracedObject class in the following example of the program. Still, it is written as a subclass of BasicObject in Ruby 1.9, so the object instance methods do not need to be explicitly deleted. It should be noted that the example code needs the object.mutex method that was previously defined; it cannot be run independently.
def synchronized(o)
if block_given?
o.mutex.synchronize { yield }
else
SynchronizedObject.new(o)
end
end
# Using method missing as a delegating wrapper class for
#thread safety
. We just extend
# BasicObject, which is defined
#in Ruby 1.9, rather than extending Object and removing our methods.
#Since BasicObject does not
# descend from Object or Kernel, it is simply
#not possible for BasicObject
methods to call top-level methods since they do not exist.
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