Michael Kovacs' photos More of Michael Kovacs' photos
Recommend Me Cable Car Software logo

Saturday, January 07, 2006

Rails realities part 4 (scope)

A short one here. I was working on a new action and view when during my POST I received the following exception:

NoMethodError (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occured while evaluating nil.[]):

The source of this error was this line:

if params[:commit] == 'Cancel'

Which confused me because params is the hash that's created by default containing all of the form elements I'm posting. A quick look over my code and I saw that I had created a local variable named 'params' in a branch of logic that doesn't even get executed on a POST. So my code looks similar to this:

def my_action

if request.get?
params = Hash.new
# do stuff
if params[:commit] == 'Cancel' # <-- NoMethodError on a nil object here
#do stuff

Renaming my local variable to something else fixed the problem so this means that despite never being executed the local variable declared 'params' is overriding the default created by rails. I'm still a newbie to ruby and rails but this doesn't seem correct to me as the 'else' clause above never executes the initialization of the new Hash. Perhaps some ruby expert out there can tell me why this is the case, or have I found a bug in rails?

major, dude. if that were really the case, it's not a bug in rails, there's a bug in rubeee. maybe your rubeee version is 1.9 or something.
Well I'm using whatever ruby is in the locomotive rails distribution for rails 1.0 which I believe is 1.8.2. It does seem hard to believe that this could be broken like this. I'm going to come up with a test case to verify as the code I put in here is paraphrased but the logic flow is exactly the same. I'll post more if I find out how it's broken especially if I end up being incorrect :-)
params != @params

" if @params[:commit] == 'Cancel' "

makes your code work.
but I didn't defined params to be a class level instance variable just a local one within the scope of my action. I'll have to give your snippet a try and see if it works but I guess I don't understand how local and instance variables work if that snippet does work.
This is how it works: As Ruby parses a method definition it needs to determine what is a method call and what is local variables (as method calls to "self" and reads of local variables look exactly the same). The way it does this is that if it encounters an assignment to the symbol in the method scope it will assume that symbol is a local variable and not a method call. You can fix this by either using a different local variable name for "params", or use "self.params", or simply by assigning "params = self.params" in the beginning of the method.

Thanks Jon! That's very good to know and it makes sense now. I guess this is one of the side effects of having local variables look the same as method calls. Not sure how I feel about this but it's not exactly intuitive :-). I wonder if it's possible to create an error message for situations like this that is clearer about the problem. I suspect not though because it seems the reader of the code needs to understand the parsing rule you described above regardless.
You can see a more pathological case on page 329 of Programming Ruby. Where its treated one way at first and then its treated another way later in the same method.

def a

for i in 1..2
if i == 2
puts a
a = 1
puts a


This is a consequence of Ruby being a 1 phase compiler I think, i.e. they only get one chance to interpret.

P.S. doesn't look like blogger retains the spaces I put in there...

Post a Comment

<< Home

This page is powered by Blogger. Isn't yours?