Ruby metaprogramming techniques

Time:2022-5-14

I have recently considered a lot of metaprogramming issues, and hope to see more examples and explanations of this technology. For better or worse, metaprogramming has entered the ruby community and has become a standard way to complete various tasks and simplify code. Since I can’t find such resources, I’m going to write some articles on general Ruby technology. This may be very useful for programmers who have moved from other languages to ruby or have not yet experienced the fun of ruby metaprogramming.

1. Use the singleton class

Many methods of manipulating a single object are based on manipulating its singleton class, and this makes metaprogramming easier. The classic way to get a singleton class is to execute the following code:

Copy codeThe code is as follows:

sclass = (class << self; self; end) 


Rcr231 recommends defining the kernel #singleton in this way_ Class method:

Copy codeThe code is as follows:

module Kernel  
  def singleton_class  
    class << self; self; end 
  end 
end 


I will use this method below.

 

2. DSL uses class methods to modify subclasses write DSL’s using class methods that rewrite subclasses

When you want to create a DSL to define class information, the most common problem is how to represent the information for use by other parts of the framework. Take defining an activerecord model object as an example:

Copy codeThe code is as follows:

class Product < ActiveRecord::Base  
  set_table_name ‘produce’   
end 

 

In this example, the interesting thing is set_ table_ Use of name. How does this work? Well, there’s a little magic involved. This is an implementation method:

Copy codeThe code is as follows:

module ActiveRecord  
  class Base  
    def self.set_table_name name  
      define_attr_method :table_name, name  
    end 
    def self.define_attr_method(name, value)  
      singleton_class.send :alias_method, “original_#{name}”, name  
      singleton_class.class_eval do   
        define_method(name) do     
          value  
        end 
      end 
    end 
  end   
end 

 

What’s interesting here is define_ attr_ method。 In this example, we need to obtain the singleton class of product class, but we don’t want to modify activerecord:: base. We achieve this by using singleton classes. We alias the original method and define a new accessor to return the value. If activerecord needs table name, it can call the accessor directly. This technique of dynamically creating methods and accessors is very common in singleton classes, especially rails.

3. Create classes and modules dynamically

Ruby allows you to dynamically create and modify classes and modules. You can make any changes on classes or modules that are not frozen. It can be useful in certain situations. The struct class is probably the best example:

Copy codeThe code is as follows:

PersonVO = Struct.new(:name, :phone, :email)  
p1 = PersonVO.new(:name => “Ola Bini”) 


This creates a new class, assigns it to personvo, and then creates an instance of the class. It is also easy to create a new class from a draft and define a new method:

Copy codeThe code is as follows:

c = Class.new 
c.class_eval do 
  define_method :foo do 
    puts “Hello World” 
  end 
end 
c.new.foo    # => “Hello World” 


In addition to struct, you can also find examples of easy class creation in soap4r and camping. Camping is particularly interesting because it has special methods to create these classes, which are inherited by your controller and view. Many interesting functions of camping are implemented in this way:

Copy codeThe code is as follows:


def R(*urls); Class.new(R) { meta_def(:urls) { urls } };   
end 

 

This makes it possible to create a controller as follows:
class View < R ‘/view/(\d+)’ 
  def get post_id  
  end 
end 


You can also create a module in this way, and then include the module in the class.

 

4. Use method_ Missing to do interesting things use method_ missing to do interesting things

In addition to closures, method_ Missing is probably the most powerful feature of ruby and one of the most easily abused. Use the method well_ Missing makes some code super simple and even indispensable. A good example is expanding hash:

Copy codeThe code is as follows:

class Hash 
  def method_missing(m,*a)  
    if m.to_s =~ /=$/  
      self[$`] = a[0]  
    elsif a.empty?    
      self[m]  
    else 
      raise NoMethodError, “#{m}” 
    end 
  end 
end 


You can use hash in this way:

Copy codeThe code is as follows:

x = {‘abc’ => 123}  
x.abc # => 123  
x.foo = :baz 
x # => {‘abc’ => 123, ‘foo’ => :baz} 


As you can see, if someone calls a method where the hash does not exist, the internal collection will be searched. If the method name ends with =, the key with the same name will be assigned.

 

Another good method can be found in markaby_ Missing skills. The following referenced code can generate any XHTML tag that contains CSS classes:

Copy codeThe code is as follows:

body do 
  h1.header ‘Blog’ 
  div.content do 
    ‘Hellu’ 
  end 
end 


Will generate:

Copy codeThe code is as follows:

<body> 
  <h1 class=”header”>Blog</h1> 
  <div class=”content”> 
    Hellu  
  </div> 
</body> 


Most of this function, especially CSS class name, is through method_ Missing sets the property of self and returns self.

 

5. Dispatch on method patterns

This can easily achieve scalability for unpredictable methods. I recently created a small verification framework. The core verification class will find all its own to check_ Start with the method and call it, so you can easily add new validation: just add a new method to the class or instance.
methods.grep /^check_/ do |m|  
  self.send m  
end 

It’s very simple and incredibly powerful. You can see that test:: unit uses this method everywhere.

6. Replacing methods

Sometimes the implementation of a method is not what you want, or it is only half done. The standard object-oriented method is to inherit and overload, and then call the parent method. It is only useful when you have control over object instantiation. Often, this is not the case, and inheritance has no value. To get the same function, you can rename (alias) the old method, add a new method definition to call the old method, and ensure that the pre and post conditions of the old method are preserved.

Copy codeThe code is as follows:

class String 
  alias_method :original_reverse, :reverse 
  def reverse   
    puts “reversing, please wait…” original_reverse  
  end 
end 


An extreme use is to temporarily modify a method and then restore it. For example:

Copy codeThe code is as follows:

def trace(*mths)  
  add_tracing(*mths) # aliases the methods named, adding tracing      
  yield 
  remove_tracing(*mths) # removes the tracing aliases  
end 


This example shows how to write add_ Tracing and remove_ A typical method of tracing. It relies on the singleton class of Article 1:

Copy codeThe code is as follows:

class Object    
  def add_tracing(*mths)      
    mths.each do |m|   
      singleton_class.send :alias_method, “traced_#{m}”, m   
      singleton_class.send :define_method, m do |*args|  
        $stderr.puts “before #{m}(#{args.inspect})” 
        ret = self.send(“traced_#{m}”, *args)  
        $stderr.puts “after #{m} – #{ret.inspect}” 
        ret  
      end 
    end    
  end 
  def remove_tracing(*mths)     
    mths.each do |m|  
      singleton_class.send :alias_method, m, “traced_#{m}” 
    end 
  end 
end 
“abc”.add_tracing :reverse 


If these methods are added to the module (a little different, see if you can write it out!), You can also add and remove tracing on classes instead of instances.

 

7. Use nilclass to implement the introduction of null object refactoring

In Fowler’s refactoring, the refactoring of “introducing an empty object” is that an object either exists or has a predefined value when it is empty. Typical examples are as follows:

Copy codeThe code is as follows:

name = x.nil? ? “default name” : x.name 


At present, Java based refactoring will recommend creating a subclass similar to null. For example, nullperson inherits from person, and the overloaded name method always returns “default name”. But in ruby, we can open classes and do this:

Copy codeThe code is as follows:

def nil.name; “default name”; end 
x # => nil  
name = x.name # => “default name” 

 

8. Learn the different versions of Eval

Ruby has several versions of evaluation. It is important to understand their differences and usage scenarios. There are Eval and instance_ eval、module_ Eval and class_ Eval several. First, class_ Eval is a module_ Alias for eval. Second, Eval is somewhat different from others. The most important thing is that Eval can only execute one string, and others can execute block. This means that Eval is your last choice to do anything. It has its use, but in most cases, you should use instance_ Eval and module_ Eval executes block.

Eval will execute strings in the current environment unless the environment already provides binding. (see article 11)

instance_ Eval will execute string or block in the context of the receiver. If it is not specified, self will be the receiver.

module_ Eval will execute string or block in the context of the called module. This is more suitable for defining new methods in module or singleton class. instance_ Eval and module_ The main difference of Eval is where the defined methods will be placed. If you use string instance_ Eval defines foo method and gets string Foo, if module is used_ Eval will get string new. foo。

module_ Eval almost always applies; Avoid Eval as you would a plague. It will be good for you to follow these simple rules.


9. Introspect on instance variables

Rails uses a technique to make the instance variables in the controller can also be used in the view, that is, to introspect the instance variables of an object. This can seriously damage the package, but sometimes it’s really easy. You can easily use instance_ variables、instance_ variable_ Get and instance_ variable_ Set implementation. To copy all instance variables from one to another:

Copy codeThe code is as follows:

from.instance_variables.each do |v|  
  to.instance_variable_set v, from.instance_variable_get(v)  
end 

 

10. Create proc from blocks and send them around

The practice of saving a proc instance in a variable and exposing it makes many APIs easy to use. This is a method markaby uses to manage CSS class definitions. It is easy to convert block to proc:
def create_proc(&p); p; end 
create_proc do 
  puts “hello” 
end       # => #<Proc …> 

It’s also easy to call:
p.call(*args) 

If you want to use proc to define methods, you should use lambda to create them. You can use return and break:
p = lambda { puts “hoho”; return 1 }  
define_method(:a, &p) 

If there is a block, method_ Missing will call block:
def method_missing(name, *args, &block)  
  block.call(*args) if block_given?  
end 
thismethoddoesntexist(“abc”,”cde”) do |*args|  
  p args  
end  # => [“abc”,”cde”] 


11. Use binding to control your evaluations

If you really need to use Eval, you can control which variables are valid. At this time, the kernel method binding is used to obtain the bound object. For example:

Copy codeThe code is as follows:

def get_b; binding; end 
foo = 13  
eval(“puts foo”,get_b) # => NameError: undefined local variable or method `foo’ for main:Object 


Erb and rails use this technique to set which instance variables are valid. For example:

Copy codeThe code is as follows:

class Holder  
  def get_b; binding; end 
end 
h = Holder.new 
h.instance_variable_set “@foo”, 25  
eval(“@foo”,h.get_b) 

 

Hopefully, these techniques and techniques have clarified metaprogramming for you. I don’t claim to be an expert in ruby or metaprogramming, just some of my thoughts on this issue.