Wednesday, February 25, 2004

Connecting the Ruby dots

I read an interview recently with Dave Thomas, one of the Pragmatic Programmers. In the article he gives an example of the power of Ruby by citing how you can essentially extend the language. When the interviewer asks him to explain he briefly tells how in Ruby you can write methods that can be invoked while a class is being defined. I know a bit of Ruby but I don't use it enough to keep fluent. As much as I thought about the example he gave (it follows) I couldn't construct in my head how his code could work. I took the opportunity today during a long CC rebase to flip through Programming Ruby and figure out more of what Dave meant. My attempt at an explination follows.


The example Dave gives is something he wrote to generate code for working with database tables.


class RegionTable < Table
table "region" do
field autoinc, :reg_id, pimary_key
field int, :reg_affiliate, references(AffiliateTable, :aff_id)
field varchar(100), :reg_name
end
end

In the above code none of the keyword like things: table, field, varchar, primary_key are part of the Ruby language. These are all extensions Dave's added to make the job of working with tables easier.


To demonstrate how this could be done I'll show a version of the Table super class that generates some simple SQL. The output of my code will look like:


CREATE TABLE region (
reg_id AutoInc PRIMARY KEY,
reg_affiliate Integer REFERENCES AFF aff_id,
reg_name VarChar(100) ,
);

I'm sure Dave's implementation is much more sophisticated than what follows but this at least captures how you might approach the problem. All the magic is in the base class Table so let's just dive in and look at the code.

class Table
def Table.table (name)
puts "CREATE TABLE #{name} (";
if block_given?
yield
end
puts ");";
end
def Table.field (type, name, *rest)
print "#{name} #{type} ", rest;
puts ",";
end
def Table.autoinc
"AutoInc";
end
def Table.int
"Integer";
end
def Table.varchar(size)
"VarChar(#{size})";
end
def Table.primary_key
"PRIMARY KEY";
end
def Table.not_null
"NOT NULL";
end
def Table.references(a1, a2)
"REFERENCES #{a1} #{a2}";
end
AffiliateTable = "AFF";
end

Most of the new keywords are just class level methods of Table. As Dave mentioned in the article the insides of a class can contain code that's executed as the class is evaluated. In Dave's example that's all there is. As the RegionTable class is being parsed the method Test.table is called. This method takes one explicit parameter but it also looks for and will execute a block that follows it. In our example the string "region" will be passed directly to the method while the following block of code will be passed as the block.

do
field autoinc, :reg_id, primary_key
field int, :reg_affiliate, references(AffiliateTable, :aff_id)
field varchar(100), :reg_name
end

Since a block might not have been passed, Ruby has a nice feature that allows a method to test whether one has and only call it if it was. In my example it's this little chunk of code.

if block_given?
yield
end

Within the block each of the 'field' lines is a separate method call to the Table.field method. The types autoinc, int and varchar are all just methods that return strings.


The only really weird bits here are the things :reg_id, :ref_affiliate , :add_id and :reg_name. These are basically string constants. In Ruby terms they are atomic strings. or Symbols. In terms of our example (at least as I've coded it) you could replace them with regular strings and it would work the same.


Like I said earlier I'm not a Ruby expert. I just wanted to understand the example Dave made a little better. If you made it this far I hope you found it informative too.


Post a Comment
 
The Out Campaign: Scarlet Letter of Atheism