Friday, January 19, 2007

Accidentally accessing accessors in Ruby

I spent about 30 minutes today trying to determine if I had been misunderstanding basic Ruby syntax for more than a year, or if I had just discovered an undocumented bug in the language. The answer was neither, of course.


Ruby variable naming is straightforward. Class instance variables begin with @, as in @user_name. Local variables are unadorned - just user_name. So, when I came across some code that a co-worker had written that did not follow this convention, and he said that it was working fine, I was initially confused. Here’s an extremely simplified example:




1 class VarTest
2 attr_accessor :instance_var
3
4 def print_it
5 puts "The instance var is #{instance_var}"
6 end
7
8 end
9
10 vt = VarTest.new
11 vt.instance_var = "fred"
12 vt.print_it
13


This code should be spitting out something about the invalid use of the instance variable instance_var, right? That should be @instance_var in that method (line 5). But there it was, working just fine.


So I experimented a bit.




1 class VarTest
2 @instance_var = "some initial value"
3
4 def print_it
5 puts "The instance var is #{instance_var}"
6 end
7
8 end
9
10 vt = VarTest.new
11 vt.print_it
12
13 # ~> -:5:in `print_it': undefined local variable or method `instance_var' for #<VarTest:0x1d4558> (NameError)
14 # ~> from -:11
15


Now that’s what I had expected the first time. After more moments of confusion than I really care to admit, the extremely obvious answer came to me. In the first example, instance_var was the accessor method, NOT the variable. Duh. In the second example, where the error was thrown, instance_var was a local variable, since no accessor had been declared. Hence, the error. Again, duh.


It does bring up an interesting point, however. When I am accessing an instance variable from within a method, I use @varname, never just varname. But, I also frequently create accessor methods in Rails models that manipulate the data in some way. This leads to the possibility of some interesting bugs. Here’s a pretty contrived example.




1 class User
2
3 def lastname=(val)
4 @lastname = val
5 end
6
7 def lastname
8 "Mr. " + @lastname
9 end
10
11 def print_name
12 puts "print_name: #{lastname}"
13 end
14
15 def print_name2
16 puts "print_name2: #{@lastname}"
17 end
18
19 end
20
21 vt = User.new
22 vt.lastname = "Kruger"
23 vt.print_name
24 vt.print_name2
25
26 # >> print_name: Mr. Kruger
27 # >> print_name2: Kruger
28


This is fine, and correct of course, unless what I really wanted was “Mr. Kruger” in both cases.



The lesson here is…


The only real lesson is to be aware of the difference between using @ and not in class methods. Best practice is probably to consistently use @, in part simply because it helps differentiate between local and instance variables. In cases where the accessor really needs to be used, as when the accessor function is performing some sort of processing, its best to use self.varname to make it clear that a function is being called.

1 comment:

Anonymous said...

I think the decision of which to use is somewhat simplified if you just think in terms of which interface you actually need. It's sort of a principle of least privilege thing. The more that a method's logic can be implemented in terms of its public interface, the better.

Even if you're not doing any computation in the accessor to begin with, it's just that much less code to alter/debug if you decide later to add some.