Monkey Patching Ruby, just like in The Matrix - Whoa! 4

Posted by jeff Sat, 01 Mar 2008 20:11:00 GMT

Update: in the comments, Eric Anderson reminds us that the common way to do this on the class-level is through mixins. You can read a good tutorial on mixins here.

Ruby has open classes. This means that your classes can learn to do new things or learn to do old things differently via monkey patching. This can be both a good thing or a bad thing depending on how the programmer chooses to use or abuse the feature.

To give an example of the openness of ruby classes, consider the following person class:

class Person
  attr_accessor :name
  
  #alias for readability joy
  alias knows? respond_to?
  
  def initialize(name)
    @name = name
  end
  
  def action(out)
    puts "*#{name} #{out}*"
  end
  
  def say(out)
    puts "#{name}:  #{out}"
  end
  
  def assert_knowledge(method)
    say 'I ' + ( self.knows?(method) ? 'know ' : 'don\'t know ' ) + method.to_s + '. '
  end
end

Kinda boring... very limited functionality. Let's imagine that we suddenly find out we're living in The Matrix. Yikes. Ok, we're over the shock. Now we're out of the matrix and someone has plugged a cord into the back of our head and wants to use some crazy disks to teach us things.

class MartialArtsLearningTape
  def teach(person)
    
    def person.kung_fu
      # ...
      action 'KA-POW\'z!'
    end
    
    def person.ju_jitsu
      # ...
      action 'HI-YA\'s!'
    end
    
    # ...
  end
end

The MartialArtsLearningTape class can be used to modify a Person to give them new abilities (methods). Consider the following summary of the plot of The Matrix.

keanu = Person.new('Thomas Anderson')
keanu.assert_knowledge(:kung_fu)
keanu.action "gets unplugged from the martix"
keanu.name = 'Neo'
MartialArtsLearningTape.new.teach(keanu)  
keanu.assert_knowledge(:kung_fu)
laurence = Person.new('Morpheus')
laurence.say 'Show me. '
keanu.say "self.methods.grep(/kung_fu/)\n=> #{keanu.methods.grep(/kung_fu/)}"
laurence.say 'uh....'
keanu.kung_fu

When you run the matrix.rb script you get...
$ ruby matrix.rb
Thomas Anderson:  I don't know kung_fu. 
*Thomas Anderson gets all matrix-ized*
Neo:  I know kung_fu. 
Morpheus:  Show me. 
Neo:  self.methods.grep(/kung_fu/)
=> kung_fu
Morpheus:  uh....
*Neo KA-POW'z!*

A better way

There's a better way to code this. Let's create a method named unplug! which lets us give a Person learning abilities when they're unplugged from the matrix:

class Person
  #...
  def unplug!(new_name = @name)
    action("gets unplugged from the matrix and is known as #{new_name}!")
    @name = new_name
    def learn(name, &block)
      self.class.send(:define_method, name, &block)
    end
  end

end

class MartialArtsLearningTape
  def teach(person)
    
    person.learn(:kung_fu) { action 'KA-POW\'z!' }
    person.learn(:ju_jitsu) { action "HI-YA\s!" }
    
    # ...
  end
end

To see this in action, check out matrix2.rb

define_method is private, so we have to put in a helper method (learn) to use it in a friendly way. For other ways of teaching classes new tricks, check out chapter 11.3 of The Ruby Way, Second Edition, and specifically 11.3.5 for define_method.

What we've learned

  • alias can be used to make our code a little more readable. Compare neo.respond_to?(:kung_fu) to neo.knows?(:kung_fu).
  • Ruby classes are open and can learn new methods.
  • Keanu Reeves can play characters other than Ted Theodore Logan and Evil Ted. Whoa.

Questions:

  • Isn't kung fu too awesome to not end in an exclamation point (i.e. neo.kung_fu!)?

    I agree with you in principle... Programming Ruby: The Pragmatic Programmers' Guide, Second Edition offers the following:

    Methods that are "dangerous," or modify the receiver, may be named with a trailing !
    Well, kung fu is definitely potentially dangerous, but in this instance the receiver is self. Rather than modifying self, kung_fu is much more likely to modify the opponent's face. It really could it could go either way, but I'll stick with the "methods that modify self" approach since the opponent is not the receiver.

  • Seriously, why are you aliasing "respond_to?"? Isn't that completely useless and wasteful?

    Meh. There's no real reason except that its more readable and more natural in this context. respond_to? seems very mechanical while knows? is a little more human. Star Trek fans should appreciate the following proper use of respond_to?:

    spot.respond_to?(:verbal_commands) # => false