Wednesday, May 24, 2006

attr_accessor meta programming

Reading Jamis Buck's article Writing Domain Specific Languages I decided to attempt his challenge to write my own attr_accessor function.

First, I was curious to see how attr_accessor was implemented in Ruby. I found the source in object.c which simply calls "rb_attr(klass, rb_to_id(argv[i]), 1, 1, Qtrue);". The "rb_attr" function is located in eval.c. If you spend a few min looking at it, you will see it has special code for handling Access Control (public, private, protected). For now, I will ignore Access Control, and just implement creating the accessor methods.

Where should attr_accessor go? Looking at the docs, you will find attr_accessor is a member of Module. To see if I had the correct location, I quickly wrote...

module Module
alias old_attr_accessor attr_accessor

def attr_accessor( *symbols )
puts "attr_accessor called with #{symbols.join(' ')}."
old_attr_accessor( *symbols )
end
end


This failed since Module is not a module, it is a class! Laughing at myself and Ruby, I corrected the definition. The code above also demonstrates how easy you can implement simple Aspect Oriented Programming in Ruby. Using alias, I give the old attr_accessor method an new name old_attr_accessor. Then overwrite attr_accessor with my own code. Any call to attr_accessor will now print out the information that it was called.

Fun so far, but I haven't really defined my own version of attr_accessor. I've simple called the old one. I found module_eval does the trick (class_eval also works since it is a synonym for Module.module_eval). The final code is...


class Module
def attr_accessor( *symbols )
symbols.each { | symbol |
module_eval( "def #{symbol}() @#{symbol}; end" )
module_eval( "def #{symbol}=(val) @#{symbol} = val; end" )
}
end
end

class Foobar
attr_accessor :foo
private
attr_accessor :bar
end

fb = Foobar.new
fb.foo = "hello"
fb.bar = "world"
puts fb.foo # >> hello
puts fb.bar # >> world


Pretty simple. Unfortunately, this demonstrates the problem where my attr_accessor doesn't know about the Access Control. Looking at the Ruby library and source, I could not find any way to query the Access Control level from within my attr_accessor. So the only solution I could find is the following hack...


class Module
alias old_public public
alias old_private private
alias old_protected protected

def public( aSymbol = nil )
if aSymbol.nil?
@__module_access_level = 'public'
old_public
else
old_public( aSymbol )
end
end

def private( aSymbol = nil )
if aSymbol.nil?
@__module_access_level = 'private'
old_private
else
old_private( aSymbol )
end
end

def protected( aSymbol = nil )
if aSymbol.nil?
@__module_access_level = 'protected'
old_private
else
old_private( aSymbol )
end
end

def attr_accessor( *symbols )
symbols.each { | symbol |
module_eval( "def #{symbol}() @#{symbol}; end" )
module_eval( "def #{symbol}=(val) @#{symbol} = val; end" )
module_eval( "#{@__module_access_level} :#{symbol}; #{@__module_access_level} :#{symbol}=" )
}
end

@__module_access_level = 'public'
end

class Foobar
attr_accessor :foo
private
attr_accessor :bar
end

fb = Foobar.new
fb.foo = "hello"
fb.bar = "world" #>> private method `bar=' called for #<Foobar:0x2855710 @foo="hello"> (NoMethodError)
puts fb.foo #
puts fb.bar #



Anyone got a better idea for handling Access Control?

2 comments:

Greg Houston said...

I could refactor the definitions of public, private, and protected into a single meta-program that generates these methods.

Maybe we need a "Replace Methods with Meta-Program" refactoring recipe.

Greg Houston said...

see my post on June 22nd about Meta Programming and Stack Traces

I should add the 2nd and 3rd arguments to method_eval so stack traces will show the correct file and line.

method_eval( "...", __FILE__, __LINE__ )