Strange Symphonies Don’t worry, be happy

15Jun/071

Ruby and Metaprogramming

Metaprogramming is something that Ruby developers always get so giddy about. Perhaps because it invokes a high level of power and control over the programming language.

Some would deem it unworldly, unsafe, and perhaps even insane.

Background Behind Metaprogramming

The first paragraph by Wikipedia describes well what Metaprogramming is all about.

Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data or that do part of the work during compile time that is otherwise done at run time. In many cases, this allows programmers to get more done in the same amount of time as they would take to write all the code manually.

Note: Emphasis by me

We've all heard about code generating code. For example defining a database schema in Propel (a PHP ORM) and letting that create the files, clsases, and all sorts of things, even the SQL for you to setup your table.

But, Metaprogramming is not that.

It's about doing it on the fly, while the program is executing.

Similarly like Open Classes, metaprogramming is not commonly found in all languages. In fact in languages that support it, people don't tend to use it.

Metaprogramming is how Rails does alot of its magic. Associations (belongs_to, has_one, has_many), Acts As (acts_as_list, acts_as_nested_set, acts_as_tree). In fact its not just limited to these, but these are the first ones that come to mind.

Don't worry if you don't grok what metaprogramming is at first. Most people didn't, including myself. In fact, I think this post can be improved, and I'll try my best to make it a valuable resource.

Simple Example

class User
  attr_accessor :name, :password
  attr_reader :gender
  attr_writer :nationality
end

For starters attr_accessor, attr_reader, and attr_writer are actually class methods. These class methods creates instance methods to access/read and mutate/assign the instance variables of the symbols stated.

  • attr_accessor — Creates both accessor and mutator methods
  • attr_reader — Creates only the accesor method
  • attr_writer — Creates only the mutator methods

This is the fully expanded version:

class User
  def name
    @name
  end

  def name=(value)
    @name = value
  end

  def password
    @password
  end

  def password=(value)
    @password = value
  end

  def gender
    @gender
  end

  def nationality=(value)
    @nationality = value
  end
end

Note:

  • @variable refers to an instance variable
  • We don't need to explicitly state 'return' as the last statement is always returned

So the 4 lines of code, expands into a 25 lines. Both blocks of codes, do the exact same thing but the first one is much more readable and maintainable.

Execution Within Classes

Actually code inside the class executes normally. If you want, you can have an if condition in there. Its legal space for you to call functions, and do other magic.

$condition = true

class User
  puts "I'm in the User class, executing your code!"

  if $condition =  true
    def self.something
      puts "I'm a Class method"
    end
  else
    def something
      puts "I'm an Instance method"
    end
  end

  puts "I'm done executing your code, and getting out of your class!"
end

Note:

  • variables starting with a dollar sign ($) are global variables
  • The self in def.self.something, refers to the class, as we are in the class scope. If it was inside a instance method, it would refer to the instance scope. This declaration is creating a class method.

When trying to execute this class with:

User.something # Calling the class method
User.new.something # Instantiate a new User object, and call the instance something method

The output is:


I'm in the User class, executing your code!
I'm done executing your code, and getting out of your class!
I'm a Class Method
Error: undefined method `something' for # (NoMethodError)

The first two line you see are the execution class executing those lines.
In the third line we call the class method.
In the fourth line we attempt to call the instance method, but it fails. Because the condition never allowed it to be created.

When we set the $condition to false. This is the following output:

I'm in the User class, executing your code!
I'm done executing your code, and getting out of your class!
Error: undefined method `something' for User:Class (NoMethodError)

It only outputs three lines, as its expecting a class method to exist, but due to the if condition, it was never created. Though the instance method will execute just fine.

Note: This is only sample code, you can do funkier things with it. It is simply to show that the execution follows within the class itself.

eval

Some may consider eval to be a sin! It lets you execute a string as if it was normal running code. That doesn't make it evil, but it can cause unexpected results if those strings are magically composed together, and didn't come out as you expected it to be.

class MagicLamp
  def self.remember_incantation(incantation)
    eval "def #{incantation}; puts '#{incantation}!'; end"
  end
end

lamp = MagicLamp.new
lamp.respond_to? :kazaam # false
MagicLamp.remember_incantation "kazaam"
lamp.respond_to? :kazaam # true
lamp.kazaam # "kazaam!"

Note: The method respond_to? is to test whether the object will "respond to" a method call, in this case kazaam. It returns false first because the method hasn't been created yet.

In this example the MagicLamp creates a instance method only when the class method remember_incantation is called. The instance method corresponds to the incantation that the MagicLamp was told to remember.

class_eval, instance_eval, module_eval

Ruby has several evals (which make it even more confusing). eval, class_eval, instance_eval, module_eval.

Why does someone need so many evals? They each execute the code within a certain context, class, instance, or module, or whatever. I won't go into further details, but its good to be aware that they exist.

The Rails Example

So here is another Rails Example using both Open Classes and Metaprogramming to make life easier ™. As you are aware, there exists the attr_accessor, attr_reader, and attr_writer for instance variables. But these methods only exist for instance variables. How about for class variables? How dare they discriminate such variables! But don't worry. With the power of Ruby you can easily fix this...mixup.

So here is an example from the ActiveSupport (v1.4.2 — corresponds to Rails v1.2.3) libraries located at activesupport/lib/active_support/core_ext/class/attribute_accessors.rb File

class Class
  def cattr_reader(*syms)
    syms.flatten.each do |sym|
      next if sym.is_a?(Hash)
      class_eval(<<-EOS, __FILE__, __LINE__)
        unless defined? @@#{sym}
          @@#{sym} = nil
        end

        def self.#{sym}
          @@#{sym}
        end

        def #{sym}
          @@#{sym}
        end
      EOS
    end
  end

  def cattr_writer(*syms)
    options = syms.last.is_a?(Hash) ? syms.pop : {}
    syms.flatten.each do |sym|
      class_eval(<<-EOS, __FILE__, __LINE__)
        unless defined? @@#{sym}
          @@#{sym} = nil
        end

        def self.#{sym}=(obj)
          @@#{sym} = obj
        end

        #{"
        def #{sym}=(obj)
          @@#{sym} = obj
        end
        " unless options[:instance_writer] == false }
      EOS
    end
  end

  def cattr_accessor(*syms)
    cattr_reader(*syms)
    cattr_writer(*syms)
  end
end

So my dear readers, how many of you understand that? Raise your hands now! Good, good, now for those of you who don't understand. Let me guide you.

The first line indicates that we are working with the class Class. Class is the class, that all classes belong to. So a String class is actually an instance of the Class class. Grok that? If not, try having a look at my post on Everything Is An Object In Ruby.

So in this block of code, we are defining 3 methods: cattr_reader, cattr_writer, and cattr_accessor.

cattr_accessor is the easy one. It just calls the other two methods. The asterisk (*sym) in an arguments list indicates that it wants all arguments from this current argument forward to be collected in an array variable, in this case sym.

If you are having a problem understanding that, pull up irb and try the following:

def args(*args)
   puts "An #{args.class} of #{args.inspect}"
end

args :symbol, 0, "array", true # An Array of [:symbol, 0 "array", true]

cattr_reader iterates through the array, and calls class_eval with the arguments for <<-EOS, __FILE__ and __LINE__.

      class_eval(<<-EOS, __FILE__, __LINE__)

The <<-EOS is referred to as a heredoc, another method of creating strings without using the single or double quotation marks. To indicate the end of the string you enter the first characters preceding the heredoc (EOS), which yup, you've guessed it! It stands for "End of String". These characters can be anything really.

__FILE__ and __LINE__ are magic constants. They aren't exactly constants per se, but indicate the current state of the execution, in which file and line it currently is in.

So class_eval is taking in a very large string which describe three components.

        unless defined? @@#{sym}
          @@#{sym} = nil
        end

The first section tests to see if the class variable has been created, and unless it has, it is assigned the value of nil. @@variable refers to a class variable.

        def self.#{sym}
          @@#{sym}
        end

The second section creates the class method to return the value of the class variable.

        def #{sym}
          @@#{sym}
        end

This final section creates the instance method to return the value of the class variable.

So whenever you call cattr_reader from an executing class. These class variable is created, unless it already has, and the class and instance methods to access that variable is created as well.

cattr_writer is slightly different in that it also accepts options, via the last argument being an Hash.

    options = syms.last.is_a?(Hash) ? syms.pop : {}

If the Hash is the last element in the array, it is simply popped off.

        def self.#{sym}=(obj)
          @@#{sym} = obj
        end

Here we are creating the class method to write to the class variable.

        #{"
        def #{sym}=(obj)
          @@#{sym} = obj
        end
        " unless options[:instance_writer] == false }

What you see here is actually a block whose result will be outputted as a string. Inside the block, you'll actually see the string contents, where it declares the method to be created. But at the very end you'll notice the unless condition, which is where the options come into play. So unless you specify a :instance_writer => true, in your arguments an instance method will be created allowing to access the class variable

Well that pretty much describes the whole file.

How to use this then?

class Polygon
  cattr_reader :sides
end

class Triangle < Polygon # this is how we inherit a class
  @@sides = 3
  @@degrees = 180
end

class Square < Polygon
  @@sides = 4
  @@degrees = 360
end

Triangle.sides # 3
Triangle.degrees # 180
Square.sides # 4
Square.degrees # 360

class Monopoly
  cattr_accessor :currency
  cattr_writer :players
  cattr_writer :max_players, :instance_writer => true
  players = 4
  max_players = 8
end

Note: Due to its brevity, this example sucks big time and it can be highly improved. But I can't think of one now. I'll replace it later.

Conclusion

You can do some really sweet things with Metaprogramming, but the examples here are not sufficient to describe what you can do. They are too succinct, and don't give a context on how it is to be used. But I hope this provides a helpful insight to the existence and usage of Metaprogramming.

At least you know that Metaprogramming exists, so if ever need be. Ruby is there for you.

Related posts

Tags

Tagged as: Leave a comment
Comments (1) Trackbacks (0)
  1. Great writeup. you should do more of these for other parts of rails


Leave a comment


No trackbacks yet.