Wisdom From Eloquent Ruby: From Oh? to Oh Yeah!, Part 3
Metaprogramming. Wait! Don’t go. As a newbie rubyist (a nubyist?), up until about a month ago that word would have inspired me to unceremoniously click the Instapaper button, where the article would then languish in digital limbo for a small eternity. As it turns out, metaprogramming is really not that bad and used correctly, has the potential to keep your code DRY and make your life and the lives of your fellow coders a bit easier in the process.
As I was writing this, I noticed this on r/ruby. The author Jonathan Jackson does a superb job of going more in depth on the hooks that I simply mention in this post. If you want to see some more good examples of hooks in action, head over there. It’s worth a read, and looks to be just the first part in a series on metaprogramming.
By the way, this is a continuation of my notes and highlights from Russ Olsen’s excellent book Eloquent Ruby. If you want to start from the beginning, here ya go.
Chapter 20: Use Hooks to Keep Your Program Informed
- A hook is a way – sometimes by supplying a block and sometimes by overriding a method – to specify the code to be executed when something specific happens. (loc. 3615)
- Defining the class method
inheritedas follows lets your code know when a subclass is defined: (loc. 3621)class BaseClass def self.inherited(subclass) puts "This #{subclass} has just been defined." end end - There is a module version of
inheritedcalledincluded, which is called when a class has included a module. A common use for this is to add some class methods to the including class as your module gets included. That was a lot of includes. (loc. 3470) - How do we get both instance methods from
includeand class methods usingextend? In our module we can define this courtesy method: (loc 3676)module FullOfMethods module FullOfClassMethods # class methods end def self.included(class) class.extend(FullOfClassMethods) end # instance methods end class OurClass include FullOfMethods end - There’s many more hooks available to investigate such as
at_exit,method_missing, orset_trace_func. (loc. 3688)
Chapter 21: Use method_missing for Flexible Error Handling
- When Ruby fails to find a method, it turns around and calls a second method,
method_missing, which accepts two arguments, the method name and the arguments the method was trying to pass. (loc. 3795) We can override this behavior:class MissingMethods def method_missing(method_name, *args) puts "You tried to call #{method_name} with the following arguments: #{args.join(', ')}. Woops?" end end - There’s a neat gem called
textwhich has a module calledSoundexthat has methods for generating a soundex object. You can combine this withmethod_missingto catch and try to correct typo’d methods, based on the fact that similar sounding words tend to
generate the same soundex code. (loc. 3918) - The method
const_missinggets called whenever Ruby detects a reference to an undefined constant, taking only a single argument, the constant. This method is actually a class method so redefining it works slightly differently. (loc. 3829)class MissingConstants def self.const_missing(const_name) puts "You're missing a constant by the name of #{const_name}!" end end - Some things to remember about
method_missingandconst_missing.- You don’t want to use it unless you really need it. Regular error handling will suffice for the majority of misplaced/misspelled methods. (loc. 3867)
- Be very careful about using them. What happens if you try to call a missing method inside of
method_missing? (loc. 3872)
Chapter 22: Use method_missing for Delegation
- Delegation in coding is done by supplying one object with a reference to an object with methods that we want. (loc 3900)
class SomeObject def initialize(original_object) @original_object = original_object end # we're going to use the original object's print_name method # to tweak our own def print_name puts "There is a better way to delegate!" @original_object.print_name end end - There is a better way to do this though by using
method_missing.class SomeObject def initialize(original_object) @original_object = original_object end def method_missing(method_name, *args) @original_object.send(method_name, *args) end endThis has the benefit of flexibly handling calls on methods that may or may not exist in the object with the methods that we want. If we call a method on an instance of
SomeObjectand Ruby detects it’s a missing method, then it tries to use that method with the our@original_object. If it doesn’t detect it there, then it will raise the exception that we expect to see. (loc 3932) - This is great until we try to call a method that has been redefined in our original object but is also a method of a superclass. For instance, if we want
@original_object‘s version ofto_s, we’re not going to get it because it’s further up the inheritance tree and not “missing”. We can fix this by making our class a subclass ofBasicObject. (loc. 3980) - Another way to fix this problem is to use the
delegatefile from Ruby’s standard library. This comes with a nifty class calledSimpleDelegatorthat will do themethod_missinggrunt work for us just by using it as a superclass. (loc. 3994)require 'delegate' class SomeObject < SimpleDelegator def initialize(original_object) super(original_object) end end
Chapter 23: Use method_missing to Build Flexible APIs
method_missinglet’s you know that someone is trying to call a method on your object, a method that does not actually exist. You can use this fact to try to figure out what the user is asking you to do and actually do it. (loc 4070) For instance:class YourClass def replace(old_word, new_word) @content.gsub!(old_word, "#{new_word}") end def method_missing(method_name, *args) method_as_string = method_name.to_s return super unless method_as_string =~ /^replace_\w+/ old_word = extract_old_word(method_as_string) replace(old_word, args.first) end def extract_old_word(name) name_parts = name.split('_') name_parts[1].upcase end endThis takes a call for a method that isn’t there, breaks it up if it follows certain rules, and creates it.
- These are called magic methods, since users can make up method names, and as long as the names comply with the rules coded into
method_missing, the methods just magically work. (loc. 4087) - Couple catches to be aware of when this technique:
- Be aware of the possibility that someone could accidentally call a method built into your class and not the one intended for by the user. (loc. 4110)
- A call to
respond_to?is only going to be aware of the real methods and not the magic ones. (loc. 4121)
- An awesome quote from Russ on why we bother to do these kinds of things: (loc. 4097)
One of the key values of the Ruby programming culture is that the look of the code matters. It matters because the people who use the code, read the code, and maintain the code matter. Good software engineering is all about making everyone’s job easier, not just because we want to go home on time but because we all want to turn out the best possible end product. So we add convenience methods, build method_missing methods, and go enormous lengths to make our APIs easy to use because programmers with easy-to-use APIs tend to have the time to craft easy-to-use – and working – systems.
Chapter 24: Update Existing Classes with Monkey Patching
- Due to Ruby’s open classes, we can redefine the way that methods work on the fly. But if we don’t want to completely screw up the original way the method worked we need to reproduce it in the new definition. The best way to do this is with the
alias_methodmethod. (loc. 4236) Here’s an example of that in action:class Fixnum alias_method :old_addition, :+ def +(other) if other.kind_of?(String) return self.to_s + other end old_addition(other) end end - Other on-the-fly techniques for changing up methods are
private :method_name,public :method_name,remove_method :method_name. (loc 4247) - A useful ActiveSupport method called
squish!, added to the String class, compresses all the white space in a string down to a single space each. (loc. 4271) - Ways monkey patching can go wrong: (loc 4285)
- Accidentally overriding an existing method.
- Patching some application class and messing with the central mechanisms of the class in an irresponsible way.
- Messing with the primary mechanisms of a critical class such as String or Fixnum, such as what I did in the earlier example, in an irresponsible way.
- Where did the term monkey patch come from? “Programming lore has it that the original name was guerrilla patching, which then morphed into gorilla patching (perhaps programmers simply can’t spell?), which somehow evolved into monkey patching.” (loc. 6785)
Chapter 25: Creating Self-Modifying Classes
- Ruby classes are executable and defined piecemeal, one method at a time. When Ruby sees a a new class, it creates a new and completely empty class. It then executes the class body. Therefore, statements such as
ifstatements are executed like normal code. (loc. 4346) - We can use this fact to create self-modifying classes. Here is an example of programming logic in a class: (loc. 4358)
class IMFMessage def display_message # super secret message self_destruct end if AGENT == "Ethan Hunt" def self_destruct puts "This message will self destruct in 5 seconds." sleep 5 message = nil # kind of anti-climatic? end else def self_destruct puts "You weren't supposed to see that..." set_off_explosive # 'MERICA! end end end - Another way to do this is with a class method: (loc. 4374)
AGENT = false class IMFMessage # other code def self.self_destruct_decision(agent) if agent == "Ethan Hunt" def self_destruct puts "This message will self destruct in 5 seconds." sleep 5 message = nil end else def self_destruct puts "You weren't supposed to see that..." set_off_explosive end end end self_destruct_decision(AGENT) endHere the class calls its own class method at the end of its definition which sets up the
self_destructmethod. This needs to be called if you plan on usingself_destruct. It’s defaulted to the second version but can easily be switched by callingIMFMessage.self_destruct_decision("Ethan Hunt"). - We can use the built-in constant
RUBY_VERSIONto make decisions based on what version of Ruby a user is using. (loc. 4403) - We can also create code that reloads itself using a method that looks like this:
def self.reload load(__FILE__) end
loadworks a lot likerequirebutrequireknows when a file has already been loaded. We can’t use that if we want to reload something, so we use the dumberloadmethod.__FILE__is always set to the path of the source file of the current class. (loc. 4416) - Russ on testing and metaprogramming: (loc 4444)
While regular code needs unit tests, metaprogramming code absolutely cries out for them!
Chapter 26: Create Classes That Modify Their Subclasses
- Inside the superclass, we can define a class method that creates instance methods inside our subclasses.
class Employees def initialize @employees = [] end def <<(worker) @employees << worker end def self.create_position(method_name, options_hash) title = options_hash[:title] pay_grade = options_hash[:pay_grade] code = %Q{ def #{method_name}(worker_name) worker = Worker.new("#{title}", "#{pay_grade}", worker_name) self << worker end } class_eval(code) end endThis is a little hard to follow but let’s say we have a subclass of Employees called Accountants. Anytime we had a new position, we would run
Accountants.create_positionwith the position name and its various options.This would then call
class_eval(code), creating a new instance method inside ofAccountantswith the contents ofcodeas the definition of the method. Here’s how it might play out.options = { :title => "Intern", :pay_grade => "Not good" } Accountants.create_position("add_intern", options) new_group_of_accountants = Accountants.new() new_group_of_accountants.add_intern("M. Lewinsky") # which adds our new worker bee into our currently empty list of workers # this also adds the method to any instances already created old_group_of_accountants.add_intern("R. Howard") - This is great but there is a better way using
define_method(better here meaning a built-in API call).define_methodtakes the method name as an argument and a block. The parameters to the block become the arguments to the method, while the block itself becomes its contents. (loc. 4522)class Employees def initialize @employees = [] end # ... def self.create_position(method_name, options_hash) title = options_hash[:title] pay_grade = options_hash[:pay_grade] define_method(method_name) do |worker_name| worker = Worker.new(title, pay_grade, worker_name) self << worker end end endBy the way, I messed around with both of these techniques a lot when writing this section. The
define_methodtechnique also ends up being better because the long-ass string/method hybrid we defined using%Qin the first technique can be confusing as hell to debug. - We can also define methods in the superclass that the subclass can call to change itself. Here’s a good example from the book:
class StructuredDocument # Rest of the class omitted def self.privatize private :content end end class BankStatement # code omitted privatize endIn this example, calling
privatizeat the end of the subclass makes the inheritedcontentmethod act differently. We wouldn’t be able to call it from an instance ofBankStatementnow. (loc. 4532) - Cool fact, the ability for a class to self-modify is how
attr_accessor,_reader, and_writerwork to create methods in your instance and how Rails’ ActiveRecord works when you use something likehas_oneorhas_many. Think about how this might work for a second. I live for these “aha!” moments. Thanks Russ. (loc. 4560)
So I said there’s three parts to this and they follow along with the book, I lied. There’s actually four parts to the book, the fourth dealing with putting everything together, but for a few reasons, this will be my last write-up on it:
1) These were not as easy as I thought they’d be (in a good way). Because this is going out on the net, I like having a pretty thorough understanding of what I’m writing (and not making a complete ass of myself). That takes time and research beyond what I read in Eloquent Ruby, and although this has been a huge value to me, I have to code sometime!
2) The last chapters are a little different because they deal with pretty specific concepts that are hard to capture in just 3-5 highlights.
3) Finally… the author put a lot of time and effort into this book. I can’t just give away the ending.
Thanks for reading guys and gals! I have learned a metric ton from doing this and I hope you’ve gained something from reading it. If you enjoyed these notes, go out and get the book!

Pingback: Google