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