Having spent the last couple weeks reviewing Pragmatic Programmer’s screencasts on metaprogramming and the Ruby object model, I discovered one thing for certain. I shouldn’t be doing any metaprogramming any time soon. It’s a tricksy endeavor frought with peril, even for seasoned Rubyists.
I kind of had a feeling that would be my conclusion based on hearing this talk at Madison Ruby by Paolo Perrotta, the guy who wrote the book on metaprogramming. In the talk, he shows just how tricky metaprogramming can make your code.
Essentially, because metaprogramming is code that writes code, you can end up with a codebase that has hidden “gotcha” methods that are created as if by magic. This often conceals what is actually going on, makes the code hard to read, hard to debug and hard to extend or modify.
Definitely an endeavor left to someone who’s a bit more of an expert than me.
That’s not to say that I didn’t learn anything. Quite the contrary. The series starts with a great overview of the Ruby object model, the call chain for methods, inheritance versus mixins, singleton methods and a whole slew of other great stuff.
I plan to cover a couple of these topics here, but as a note to the reader, a lot more has been written on these topics by people infinitely smarter than me. My coverage of the topics here is intended to be more of an exercise of completion of the section of my 1up project than an attempt at a definitive screed on the subject.
The Call Chain
When you call a method in Ruby, the interpreter looks for the definition of that method in the object that maps to ‘self’ in the current context, if that method doesn’t exist on self, Ruby moves “up the chain” and checks self’s parent class, and so on until it runs out of parent classes. Here’s an example:
# define a specific string my_string = "I'm a string!" # call #reverse # 1) this first checks to see if my_string has a method called reverse # 2) it doesn't find one # 3) it then checks for reverse on my_string's parent class, which is String # 4) it finds the method and executes it and outputs the following: my_string.reverse => "!gnirts a m'I" # if we define a singleton method on my_string like so: def my_string.reverse "I'm overriding .reverse" end # and then call .reverse on my_string my_string.reverse # we get the result of the new method and not that of # the default behavior of String my_string.reverse => "I'm overriding .reverse"
This concept becomes important in metaprogramming because the Ruby interpreter has several “hooks” for methods, classes and modules, marshalling and coercion that can be used to execute code that extends Ruby’s functionality. When you use these hooks, you need to be sure where exactly which member of the call chain you’re attaching your meta-functionality to. For example, in the screencasts, Dave Thomas demonstrates how to attach console tracing to methods by using the method_added hook. It’s really impressive to watch him implement the code, but there are a lot of nuances that he has to work around. A list of all these hook methods can be found here: http://stackoverflow.com/questions/5127819/is-this-a-comprehensive-list-of-ruby-hook-methods.
One of the more well-known uses of these hooks is to use the method_missing hook to create the semantically-rich find_by_xxxx methods in Active Record where xxxx is a model attribute that is essentially a column in a database. (I sure hope I said that right)
You must realize the truth: there are no class methods
Another interesting tidbit that stuck with me was this: class methods are really nothing all that special. In actuality class methods are really just singleton methods on a Class object. The following two examples are essentially the same:
class Foo def self.bar puts "I'm at the bar!" end end # is essentially the same as Foo = Class.new def Foo.bar puts "I'm at the bar!" end
I’m sure there was a ton of information in these screencasts that just didn’t stick. It’s my hope that I’ll learn more on subsequent viewings and more practice. I’d definitely recommend them to anyone who wants to learn more about the Ruby object model and how important it is to have a deep understanding of those concepts before embarking on any metaprogramming adventures.
Also, if I’ve made any egregious errors in my explanation of concepts above, please let me know. I really want to keep this stuff straight in my head.
For my next topic, I’m going to tackle Test Driven Design (TDD) with Ruby and Rspec in an effort to write cleaner, better-designed code. I’m always amazed when I watch some of the giants in the industry sit down and drive out features and functionality with TDD, and I’m looking forward to a lot of practice.