« Back to posts

Seven Languages: Week 1 (Ruby) - Day 3

15 Dec 2011

Day 3 is about metaprogramming: writing code that writes code. Now we are really getting into the meat of Ruby, exploring some of the things that things that set it apart from other languages. Ruby has a powerful and varied toolset to make metaprogramming easy and natural. It gives you full control to tweak and patch built in classes, and even makes it convenient. These tools(SYN) can combine with its flexible syntax to let you create readable internal DSLs especially easily.

(This article is part of a series of posts I am doing about my journey through the exercises of the book Seven Languages In Seven Weeks. The article previous to this one is Week 1 (Ruby) - Day 2. For an overview see the Seven Languages project page.)

Topics covered

This was a fairly quick week, with only a few topics and one homework question. Not that this is a bad thing necessarily – in the end it turned out to be a nice break before the tough stuff really started. The main three topics in Day 3 were: open classes, method_missing, and mixins.

Open classes

In Ruby you have the power to modify or add to any class, even the built-in ones. You can open up the String class and add a new method called quack(), or redefine size() to return a random integer. Using this power without restraint can lead to pretty impressive messes of spaghetti code.

Modifying existing classes at runtime in this way has become known as monkey patching, and also (my favorite) duck punching:

Well, I was just totally sold by Adam, the idea being that if it walks like a duck and talks like a duck, it’s a duck, right? So if this duck is not giving you the noise that you want, you’ve got to just punch that duck until it returns what you expect. – Patrick Ewing

You’ve got to just punch that duck.

Method_missing

This special method gets called any time a method that doesn’t exist is called. If you implement it you have access to the name and arguments of the method that was called. In the book, Bruce Tate gives an example of using this to create a nice way to specify roman numerals. The result is that you can say roman.X, roman.III, roman.CXII, etc.

I saw another example of method_missing’s power in Chapter 8 of The Ruby Programming Language, where a simple DSL for generating html is created. This is what it looks like in use:

pagetitle = "Test Page for XML.generate"
XML.generate(STDOUT) do
  html do
    head do
      title { pagetitle }
      comment "This is a test"
    end
    body do
      h1(:style => "font-family:sans-serif") { pagetitle }
      ul :type=>"square" do
        li { Time.now }
        li { RUBY_VERSION }
      end
    end
  end
end

The implementation is fairly short: they create a basic XML class in only 53 well-commented lines. If you’re interested, the code examples are available for inspection from David Flanagan’s website. For convenience, here is a link directly to this snippet of code.

Mixins

Mixins are a way for a class to include code from a module inside itself. Imagine a class including a module with a Meow() method. This would let you call Meow() on any instance of that class. Combined with open classes and the ability to add or change methods at runtime, this makes it easy to package and use code that changes code.

As I understand it, ActiveRecord in Rails is sort of the poster child for how metaprogramming using mixins. It will customize your domain models at runtime with many things, including accessors using the column names from the database. I admit I don’t know a lot about the details of this area, but there is a whole book dedicated to metaprogramming in Ruby that I plan on reading someday.

What was missing

Surprisingly, an entire category of useful metaprogramming tools are are almost skipped right over in Day 3. There are a lot of great methods to interact with and create methods and classes on the fly, and these are only given a passing mention in one of the examples. Methods like define_method, alias_method, the different versions of eval, or methods.grep.

With these tools Ruby gives you, without much effort you can do things like: find all methods that have a name like /check.*/, systematically rename those methods, and replace them with a method that does something (like logging) and THEN execute the method.

Ola Bini has a great post about metaprogramming in Ruby that talks about these techniques and more. I recommend it if you have been at all interested this post so far, it is well worth reading.

Highlights from exercises

There was not very much to do for the homework questions this week, unfortunately, just one fairly short exercise. This day in particular was one that I think could have used some really cool examples. Metaprogramming has a lot of depth and exciting possibilities, and this chapter could have shown that off more with better exercises.

Anyway, the exercise for this day shows some basic usage of method_missing. The existing ActsAsCsv module is given to you to extend, so the most interesting bit of code in the exercise is the definition of method_missing in CsvRow. It takes the name of the method and attempts to return the rows from the column that has that name.

def method_missing name, *args
    content_index = @header_row.index(name.to_s)
    return @content_row[content_index]
end

Full solution

Here is a nicely formatted version of my solution to the exercise from Day 3 of Ruby. The home of the following code is on github with the other exercises.

Do:

1. Modify the CSV application to support an each method to return a CsvRow object. Use method_missing on that CsvRow to return the value for the column for a given heading.
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_rows

        def read
            @csv_rows = []
            file = File.new(self.class.to_s.downcase + '.txt')
            @headers = file.gets.chomp.split(', ')

            file.each do |row|
                csv_contents = row.chomp.split(', ')
                @csv_rows << CsvRow.new(@headers, csv_contents)
            end
        end

        def initialize
            read
        end

        def each &block
            @csv_rows.each &block
        end

    end

    class CsvRow
        attr_accessor :header_row, :content_row

        def initialize(header_row, content_row)
            @header_row = header_row
            @content_row = content_row
        end

        def method_missing name, *args
            content_index = @header_row.index(name.to_s)
            return @content_row[content_index]
        end
    end
end

class RubyCsv 
    include ActsAsCsv
    acts_as_csv
end

csv = RubyCsv.new
csv.each {|row| puts row.one}
Output (given the sample file in my github repo)
lions
han
chewbacca
r2 d2
anakin skywalker
leia organa
threepio
jawa
emperor palpatine
darth sidious
bail organa
vader

Next in this series: Day 1 of Io

blog comments powered by Disqus
Content by Nick Knowlson: Google+
rsstwitter