Week1 Day 3 – Ruby metaprogramming

Seven Languages in Seven Weeks really does more than scratch the surface of a language. It shows off what each language does best. Ruby is great at expressive metaprogramming, so we wasted no time into diving deep into not one, but three types of metaprogramming:

  1. Opening an existing class to add extra methods
  2. Using method_missing to define dynamic methods
  3. Creating mixin modules to extend functionality

For me, this was highly insightful. Metaprogramming is always something that Other People do. I have written a couple of gems before, but i didn’t really understand what i was doing – i just played around copying examples until it did what i wanted. Or more often, i would be quick to use inheritance, not realising that a mixin module might serve me better.

I was inspired by Seven Languages in Seven Weeks where it showed how to start with a simple module and then adapt it a little to make it even more flexible. In future i will refer to this book to guide me as it explains so well what’s going on.

module ActsAsCsv

  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def acts_as_csv
      include InstanceMethods
    end
  end

  module InstanceMethods
    attr_accessor :headers, :csv_contents

    def initialize
      read
    end

    def read
      # ...
    end
  end

end

class RubyCsv
  include ActsAsCsv
  acts_as_csv
end

We write a module which, once included, the first thing it does is adds another module to the class which included it. That module provides a macro method which, when called, adds instance methods and attributes.

I now realise that there is nothing special about the module names ClassMethods and InstanceMethods. They are just named that way for clarity. There is not really any voodoo going on here.

When trying to add an each method i started off like this:

m = RubyCsv.new
m.each{|row| puts row.inspect }

I implemented an each method like this:

def each(&block)
  @csv_contents.each {|r| block.call(r) }
end

Sure enough, i was able to inspect the row. But then we wanted to do this, where one of the headings in the CSV file was ‘one’:

m.each{|row| puts row.one }

“No problem!” i thought. “Time for a method_missing” … but then i realised something i definitely would not have anticipated without doing the exercise.

undefined method `one' for ["lions", "tigers"]:Array (NoMethodError)

There you see the problem?! I’m working with an array at this point! Looking back at the book, it gave me a hint, which i had missed:

Modify the CSV application to support an each method to return a CsvRow object.

Ah, so that’s the answer! I modified my each method to do this:

def each(&block)
  @csv_contents.each do |r|
    row = CsvRow.new(@headings, r)
    block.call(row)
  end
end

I had to decide where to put the CsvRow class, and decided to put it as part of the module so there’s nothing else i had to require when mixing in the module.

I needed to pass the headings found, so that the row could implement its method_missing and find the right position in the array. The CsvRow class ended up like this:

class CsvRow
  attr :contents, :headers

   def initialize(headers, contents)
    @headers = headers
    @contents = contents
  end

  def method_missing(method)
    index = @headers.index(method.to_s)
    raise "No heading '#{method}' in the CSV file." if index.nil?
    @contents[index]
  end

end

The full solution can be found here: week1-ruby/acts_as_csv_module.rb

I am really glad of the opportunity to explore and understand Ruby metaprogramming a lot better. I am also extremely excited for what i’m going to learn in the other six languages in the next six weeks. This has set me up to expect some really awesome things!

Advertisements

5 comments on “Week1 Day 3 – Ruby metaprogramming

  1. Did the book explore the pitfalls of method missing too? Namely, that it muddies the interface to your class, especially when inheriting from a class that implements method missing as well. It’s also very easy to include subtle bugs in method missing too. If possible, you should prefer to dynamically define real methods, but if you can’t avoid method missing remember to call super() if your class cannot handle the message passed. Also, remember to override respond_to? to mirror method_missing’s “can I handle this message” logic. The best way to do that is to factor the message analysis logic into a private instance method.

    • That’s really great advice, thank you. No, the book didn’t go into any of that. I think it should have done because it’s giving you the power to do some rather dangerous things, and if you don’t know how to handle that power responsibly, you could get into a lot of trouble!

      • It’s not necessarily dangerous, but the ruby philosophy is to remove all barriers so you always need to be aware of the trade offs you are making. Having said that, I strongly encourage you to use and abuse metaprogramming in a safe environment so you can find what works best.

  2. Thanks for your concise solution. It’s so much more elegant than the others I have researched on-line!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s