Ruby Metaprogramming Simplified

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.
About these ads

16 thoughts on “Ruby Metaprogramming Simplified

  1. Hmm I think Meta would be a better name than RMeta.
    I used to put a leading R on many of my classes (like “RSource” for some compiler), but one day I realized that a new, more fitting name is really better than those leading R.. )

    But anyway, is there a clear use case for it?

    The simpler the syntax the easier it will be for people to get to what they need/want to, but often it seems as if people do not have a clear use case scenario – they “just use what is there” in a language, even if it isn’t really needed, or does not give a big advantage.

    • Mark,

      concerning the leading ‘R’ I agree with you. To be honest, it was the first name that popped into my mind and it never changed from that point on. Simply, because ‘RMeta’ is a descriptive name for what it accomplishes, only the ‘R’ is redundant.

      The use-case I have in mind is related to my upcoming post which is all about intercepting method calls for various reasons (e.g. profiling, before, after events). While implementing that framework, my code became very clouded by unreadable statements (meta-programming statements). That’s when I decided to factor it out with the additional benefit of think-once-use-n-times :)

      Best regards,
      Christoph

  2. This is a very neat post. I keep a .txt file with all the bizarre metaprogramming syntax around because it is so irregular. Having a library to make it explicit is a great idea.

    Another feature that would be great to add to this would be aspects. I wrote a book on Ruby on Rails (The Art of Rails) and one of the chapters tries to explain metaprogramming using “wrappers” around functions, similar to the before_filter, after_filter, and around_filter operations in Rails.

    Imagine being able to say:

    RMeta.wrap String.class, :method => :to_a, :with => :timing_code

    or something to that effect. Would be very nice. I’d be willing to pitch in if you want.

    • Ted,
      thanks for your input. I used to have such a file as well until these days :)

      The code is released under BSD license, so your are free to modify it in any way you like. I’d be glad if you pick up on the code to improve it so it is useful to even more people.

      That said, I like your idea of meta-wrappers to provide aspects. I’d like to see this in action :)

      Best regards,
      Christoph

  3. Christoph,

    Brilliant idea! Its one of those “Why didn’t I think of that?: moments. :-)

    However this seems to provide clarity and consitency at the price of verbosity. I wonder if it would be worth it to monkey-patch Object to enable a simpler syntax (hey, we’re already Meta :-).

    – Ernie P.

    • Ernie,

      thanks for your feedback.

      On the one hand, I agree with you that monkey-patching Object would reduce verbosity, but on the other hand, I tend to get nervous when patching Object by default. I’d rather let the user to choose from the two options: but RMeta code into a module Meta, extending RMeta class by Meta and optionally including Meta at Object level.

      Best regards,
      Christoph

  4. I fail to see how learning the syntax of some additional library to accomplish fairly basic programming operations is better than learning the existing Ruby syntax for the same thing.

    It looks like needless indirection.

    Time would be better spent just learning the language.

    • James,

      point taken. I learnt the syntax once, had to start all over when I needed it again another time. That’s wasted time in my opinion. RMeta is just an attempt, by no means a complete one, to increase the learning curve and get things done more quickly.

      - Christoph

  5. Neat. I remember _why’s discussion on metaprogramming introducing the concept of abstracting all the metaprogramming code into its own library. But what you have done is admirable.

    Good work.

  6. Pingback: Top Posts — WordPress.com

  7. Your RMeta library is a poorly contrived leaky abstraction. I don’t mean this as a personal attack, but I think it’s horrible to suggest use of this. Creating such a project is a great personal learning experience and best left at that.

  8. Pingback: Introducing RCapture « Christoph Heindl

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s