Monkey business and how to track monkey-patching of classes in Ruby

To celebrate my 2 month anniversary with Ruby, I thought I’d play around a bit with its metaprogramming features. A quick intro: Ruby supports what some simian-primate aficionados would call monkey-patching.

Monkey patching is a way to extend or modify the runtime code of dynamic languages (e.g. Smalltalk, Javascript, Objective-C, Ruby, Perl, Python, Groovy etc.) without altering the original source code.

One way to do this in Ruby is by taking advantage of the fact that it supports “open classes.” That means that you can re-define parts of the class after it has already been defined. For example, you would be able to add a new to_s method to a class by doing the following:

class AwesomeClass
    def initialize(value)
        @value = value
    end
    def to_s
        @value.to_s
    end
end

ac = AwesomeClass.new("Soggy bacon")

puts ac # prints "Soggy bacon" 

# Change AwesomeClass's to_s implementation 
# who likes soggy bacon?)
class AwesomeClass
    def to_s
        "Chunky bacon!"
    end
end

puts ac # prints "Chunky bacon!"

In Ruby, all classes are simply instances of the Class type.  Whenever you define a method on a class, the Class instance’s method_added method is called with the name of the method being defined.  For example, if we did the following, String.method_added() would be called with “l33ticize” as the argument:

class String
   def l33ticize
       # l33ticize the string - left as        
       # exercise for the reader...that's        
       # you! :)
   end
end

Monkeys Watching Monkeys

By using open classes, we can re-define method_added for instances of Class and do some custom stuff every time a method is defined for any class.  In this example, we’re re-defining method_added so that it tracks where the method was last defined.

#!/usr/bin/env ruby                                                                                                                                                           

class Class
  @@method_history = {}

  def self.method_history
    return @@method_history
  end

  def method_added(method_name)
    puts "#{method_name} added to #{self}"
    @@method_history[self] ||= {}
    @@method_history[self][method_name] = caller
  end

  def method_defined_in(method_name)
    return @@method_history[self][method_name]
  end
end

class SomeClass
  attr_accessor  :value
  def initialize(value)
    @value = value
  end

  def compute
    return @value * 2
  end
end

s = SomeClass.new(10)

puts s.compute # Prints "20"

# Prints this file + line number from 
# the @value * 2 definition
p SomeClass.method_defined_in(:compute)
class SomeClass
  def compute
    return @value * 3
  end
end

puts s.compute # Prints "30"

# Prints this file + line number from
# the @value * 3 definition
p SomeClass.method_defined_in(:compute)

For more fun…

Comments are welcome.  There are definitely cleaner ways to implement the above.  For example, instead of storing an @@method_history variable on Class, you could store it for each Class instance (e.g. SomeClass.method_history).

What kinds of style guidelines should one follow when doing these kinds of things?  Was it a good choice to use open classes instead of using class_eval, send, or even instance_eval?  I don’t really have the answer right now, but it’s definitely on my list of things to think about while I brush my teeth in the mornings.

If anyone’s out there, I invite you to do any of the following:

  • Implement the l33ticize method
  • Improve on the method_defined_in implementation
Advertisements

November 27, 2008. Tags: , . Uncategorized. Leave a comment.