Saturday, November 04, 2006

Ruby Domain Specific Languages - The Basics (Part 1)


I have been working on a prototype of a Ruby domain specific language for one of my clients, a very large financial services company. I have learned a whole bunch of really interesting lessons, which I will share over a short series of posts as I make more progress.

The first thing I tried to do was create a basic bit of DSL tastiness like this:


class Dog < Animal
number_of_legs 4
end

class Bird < Animal
number_of_legs 2
end

class Snail < Animal
number_of_legs 0
end



My first implementation of the Animal class looked like this:


class Animal
attr_accessor :number_of_legs

def self.number_of_legs(number_of_legs)
@number_of_legs = number_of_legs
end
end




Just looking at this bit of code, it seems like it should work. However, when trying this, it doesn't work as expected.


pet = Dog.new
p pet.number_of_legs # prints nil




In Ruby EVERYTHING is an object. This includes the class objects themselves, not just the instances of class objects! This means that when the number_of_legs method is called, it is actually being called before the actual instance object itself has been created. We need to get to our instance variable, and one way is to copy the class-level instance variable to the instance itself when the actual instance is being initialized like this:


class Animal
attr_accessor :number_of_legs

def self.number_of_legs(number_of_legs)
@number_of_legs = number_of_legs
end

def initialize
instance_variable_set("@number_of_legs", self.class.instance_variable_get("@number_of_legs"))
end
end


Now the class works as expected:


pet = Dog.new
p pet.number_of_legs # prints 4




As you add new things to your DSL, having to copy each variable manually is boring. The final change is to copy every class instance variable automatically at initialization, as follows:


class Animal
attr_accessor :number_of_legs

def self.number_of_legs(number_of_legs)
@number_of_legs = number_of_legs
end

def initialize
self.class.instance_variables.each do var
instance_variable_set(var, self.class.instance_variable_get(var))
end
end
end


This is a lot cleaner, and is much more maintainable code.

2 comments:

Ron Phillips said...

In the Animal class, could that be an attr_reader just as well, since you are overwriting the attr_writer portion?

I am really asking; I tried it, and it seemed to work, but I didn't know what errors might occur anyway.

Ron

Ron Phillips said...

This line didn't work until I put pikes around "var", like: self.class.instance_variables.each do |var|

Was that necessary, or did I miss something elsewhere that made the pikes superfluous?

BTW, this is a very helpful tut: thanks a lot!

Ron