For an upcoming post, I needed to get my hands on Ruby’s metaprogramming capabilities. As always, I had a hard time to get the semantics and the syntax involved right. So, this time I decided to come up with my own approach to ease unleash the power of Ruby’s metaprogramming.



Simply derived from the fact that there is a variety of tutorial alike articles on metaprogramming in Ruby leads me to the conclusion that this matter is not easy to grasp. Partly due to the fact what metaprogramming is all about and partly due to the syntax and semantics involved.

Metaprogramming, as defined by Wikipedia, is

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) […]

Some languages allow metaprogramming techniques to be applied at compile time, for example C++. Others, including Ruby, allow metaprogramming at runtime.

Simplified Introduction

In Ruby the behavior of individual objects or the entire population of objects (i.e the associated class) can be changed. This choice will be called the :scope and can take on the values of :single and :all . Besides choosing the :scope , we have to decide whether our changes will affect methods/variables of objects (instances of classes) or classes. This choice will be called the :level . The :level can take on the values of either :instance or :class .

To make my point clear, here’s an example.

# s is an instance of String # String.new is an instance method of class String s = String.new # to_a is an instance method of s s.to_a

Usage of RMeta

To simplify these scenarios I came up with RMeta . Here it is in action

require 'lib/meta' s = String.new # Add an alias for to_a to s only RMeta.eval s, :level => :instance do alias_method :to_array, :to_a end s.to_array "another string".to_array # => no such method # Add alias to String.new RMeta.eval String, :level => :class alias_method :my_new, :new end r = String.my_new

The examples above all skip the scope parameter. By default RMeta applies the :scope :all , when an instance of Class is passed, like String , and uses :single otherwise. To explicitly specify the scope one would use

require 'lib/meta' s = String.new # Add an alias for to_a to the entire population (String) s is part of RMeta.eval s, :level => :instance, :scope => :all do alias_method :to_array, :to_a end "another_string".to_array

RMeta will always assume a :level of :instance if not specified otherwise.

require 'lib/meta' s = String.new # Add an alias for to_a to s only RMeta.eval s do alias_method :to_array, :to_a end s.to_array

Implementation

To get RMeta working download the code from the link at the bottom. The following listing only contains the essentials of RMeta .

# Simplify ruby meta programming class RMeta # Evaluate def RMeta.eval(t, params = {}, &block) if (t.instance_of?(Class)) params.reverse_merge! :level => :instance, :scope => :all RMeta.eval_class(t, params, &block) else params.reverse_merge! :level => :instance, :scope => :single RMeta.eval_obj(t, params, &block) end end private # Evaluate for instance of class def RMeta.eval_obj(obj, params, &block) case params[:scope] when :single # Affect obj only RMeta.eval_meta(RMeta.singleton(obj), params[:level], &block) when :all # Affect all instances RMeta.eval_meta(obj.class, params[:level], &block) end end # Evaluate for class def RMeta.eval_class(cls, params, &block) RMeta.eval_meta(cls, params[:level], &block) end # Access singleton class of obj def RMeta.singleton(obj) class << obj self end end def RMeta.eval_meta(cls, level, &block) case level when :instance cls.class_eval(&block) when :class class << cls self end.class_eval(&block) end end end

Links

RMeta – Implementation of RMeta along with some unit tests.