Wisdom From Eloquent Ruby: From Oh? to Oh Yeah!, Part 3

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 inherited called included, 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 include and class methods using extend? 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, or set_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 text which has a module called Soundex that has methods for generating a soundex object. You can combine this with method_missing to 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_missing and const_missing.
    1. 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)
    2. 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
    end

    This 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 SomeObject and 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 of to_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 of BasicObject. (loc. 3980)
  • Another way to fix this problem is to use the delegate file from Ruby’s standard library. This comes with a nifty class called SimpleDelegator that will do the method_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_missing let’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
    end

    This 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:
    1. 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)
    2. 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_method method. (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 if statements 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)
    end

    Here the class calls its own class method at the end of its definition which sets up the self_destruct method. This needs to be called if you plan on using self_destruct. It’s defaulted to the second version but can easily be switched by calling IMFMessage.self_destruct_decision("Ethan Hunt").

  • We can use the built-in constant RUBY_VERSION to 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

    load works a lot like require but require knows when a file has already been loaded. We can’t use that if we want to reload something, so we use the dumber load method. __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
    end

    This 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_position with the position name and its various options.

    This would then call class_eval(code), creating a new instance method inside of Accountants with the contents of code as 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
    end

    By the way, I messed around with both of these techniques a lot when writing this section. The define_method technique also ends up being better because the long-ass string/method hybrid we defined using %Q in 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
    end

    In this example, calling privatize at the end of the subclass makes the inherited content method act differently. We wouldn’t be able to call it from an instance of BankStatement now. (loc. 4532)

  • Cool fact, the ability for a class to self-modify is how attr_accessor, _reader, and _writer work to create methods in your instance and how Rails’ ActiveRecord works when you use something like has_one or has_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!