Friday, June 30, 2006

Thursday, June 22, 2006

Ruby: Meta Programming and Stack Traces

A couple of times I have run into trouble debugging or tracing a method in Ruby. Usually you can just call Kernal.caller to get a stack trace. But what if the method was generated? You don't get the correct location. For example:

class Kung
mstr = %-
def foo
puts 'Hello World from Kung.foo'
puts caller(0)
end
-
module_eval mstr
end

Kung.new.foo


Which generates the following output:

Hello World from Kung.foo
(eval):4:in `foo'
Kung-foo.rb:11


The stack trace only shows "(Eval):4:in 'foo'" which is almost useless. The "(Eval)" is a clue that the method was dynamically created using meta-programming. In this simple example, it is easy to find the dynamic code since it is near the caller "Kung-foo.rb:11". However in a real project it is frequently located far away, possibly in other source files.

To fix the stack trace, the author should use the optional arguments to method_eval as follows:

class Monkey
line, mstr = __LINE__, %-
def see
puts 'Hello World from Monkey.see'
puts caller(0)
end
-
module_eval mstr, __FILE__, line
end

Monkey.new.see


The output now shows the correct line number and file name:

Hello World from Monkey.see
Monkey-see.rb:5:in `see'
Monkey-see.rb:11


Update after reading the code in ActiveSupport core_ext\attribute_accessors.rb I found a nice way to do the above with fewer lines of code:

class Monkey
module_eval(<<-EOS, __FILE__, __LINE__)
def see
puts 'Hello World from Monkey.see'
puts caller(0)
end
EOS
end

Monkey.new.see

Monday, June 19, 2006

Ruby Class Variables, Attributes and Constants

Ruby Class Variables, Attributes and Constants

(For a related post, see Use Class Instance Variables Not Class Variables)


Writing a little ruby code the other day, I wanted to use a class variables but it didn't behave as expected.


01: class Class_Variable
02: @@var = 1
03: def Class_Variable.report; @@var end
04: end
05:
06: class Child_V < Class_Variable
07: @@var = 2
08: end
09:
10: puts Class_Variable.report #=> 2
11: puts Child_V.report #=> 2
12:


I was surprised by the result. Most other languages would have the child class shadow the parent field, but Ruby shared it! Ruby does provide Class Attributes and Constants as alternatives, but each has it's own symantics.

I threw together the following code to show the differences.


01: class Class_Variable
02: @@var = 1
03: def Class_Variable.report; @@var end
04: end
05:
06: class Child_V < Class_Variable
07: @@var = 2
08: end
09:
10: puts Class_Variable.report #=> 2
11: puts Child_V.report #=> 2
12:
13:
14:
15: class Class_Attribute
16: @var = 1 #class attribute
17:
18: def initialize
19: @var = 2 #instance attribute
20: end
21:
22: def report
23: @var # instance attribute, not the class attribute
24: end
25:
26: def Class_Attribute.report
27: @var # class attribute
28: end
29: end
30:
31: class Child_A < Class_Attribute
32: @var = 3
33: end
34:
35: puts Class_Attribute.report #=> 1
36: puts Class_Attribute.new.report #=> 2
37: puts Child_A.report #=> 3
38: puts Child_A.new.report #=> 2
39:
40:
41:
42: class Class_Constant
43: VAR = [ 'a' ]
44: VAR2 = [ 'b' ]
45:
46: def self.report
47: VAR2[0]
48: end
49: end
50:
51: class Child_C < Class_Constant
52: VAR2 = [ 'c' ]
53: end
54:
55: puts Class_Constant::VAR[0] #=> 'a'
56: puts Class_Constant::VAR2[0] #=> 'b'
57: puts Class_Constant.report #=> 'b'
58: #puts Child_C::VAR[0] #=> uninitialized constant error
59: puts Child_C::VAR2[0] #=> 'c'
60: puts Child_C.report #=> 'b'
61:


First notice that Class Variables are shared with their subclass (lines 1-11) . This differs greatly from Java and C# which shadow inherited variables.

The Class Attributes and Constants are "class private". That is, the child has no access to the parent attribute/constant with the same name. Class methods however are inherited. So calling "report" for Child_A and Child_C displays a significant difference between using Class Attributes and Constants (lines 37 and 60).

Unfortunately none of these alternatives matches the behavior seen in other languages. This can cause some confusion when going from Java or C# to Ruby. Class Attributes using accessor methods is the closest match to other languages. However the syntax similarities between class attributes and instance attributes can cause problems (lines 16, 19, 23, 27).

Tuesday, June 06, 2006

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?

Tuesday, May 16, 2006

Rubyham

It has been over a month since my last post. I have been playing around with Ruby Rails in the little spare time I can find. Sometime I might post more of my experiences with it.

In breaking news, Birmingham is getting a Ruby users group named Rubyham. Kicked off by Josh Adams, Kevin Russell and Myself. It will be fun!

Playing with Rails, I used the Act As Authenticated plugin. The plugin seems to work fine. When adding the User Activation feature, I found a few changes were needed to make me happy. I have posted them back to the User Activation page. Here is the revision with my changes at the bottom.