I was trying to read some Rails code today and came across the definition of the link_to view helper, which looks like this:
def link_to(*args, &block) if block_given? options = args.first || {} html_options = args.second concat(link_to(capture(&block), options, html_options)) else name = args.first options = args.second || {} html_options = args.third url = url_for(options) if html_options html_options = html_options.stringify_keys href = html_options['href'] convert_options_to_javascript!(html_options, url) tag_options = tag_options(html_options) else tag_options = nil end href_attr = "href=\"#{url}\"" unless href "<a #{href_attr}#{tag_options}>#{name || url}</a>" end end
At first glance, I thought for sure I had found a bug! The variable, href, is only initialized if html_options is specified. It seems like the “href_attr = … unless href” line would blow up otherwise, since it’s testing a variable that may not have been set. Or, so I thought. It turns out that my understanding of Ruby’s local variable semantics was wrong, as demonstrated by this simple test:
irb(main):001:0> x NameError: undefined local variable or method 'x' for main:Object from (irb):1 from :0 irb(main):002:0> if false irb(main):003:1> x = 0 irb(main):004:1> end => nil irb(main):005:0> x => nil
It seems that assigning to an uninitialized variable in code that does not execute is sufficient to create that variable and assign it a default value of nil. This is in contrast to Python, which doesn’t define new variables unless the code that sets them actually executes:
>>> x Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'x' is not defined >>> if False: ... x = 0 ... >>> x Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'x' is not defined
Neither does Javascript:
js> x typein:1: ReferenceError: x is not defined js> if (false) x = 0; js> x typein:3: ReferenceError: x is not defined
Is it just me, or is Ruby’s behavior completely bizarre here?
There is no right or wrong here, given different background, everything can be right or wrong. If you are really interested in this kind of quirks, the ultimate book “The Ruby Programming Language” from OReilly is what you need. Here is an excerpt that answers your question:
In general, therefore, attempting to use a local variable before it has been initialized results in an error. There is one quirkâa variable comes into existence when the Ruby interpreter sees an assignment expression for that variable. This is the case even if that assignment is not actually executed. A variable that exists but has not been assigned a value is given the default value nil.
Thanks for the book recommendation. I don’t have much Ruby background, and I’m sure it would help to learn about these types of issues beforehand.
Having spent a lot of time maintaining applications written in PHP, I have grown very defensive about uninitialized variables. It seems like there is always a bug lurking nearby.
In this case, there is no bug, and yet it still looks wrong to me. Whether it is surprising or not depends on your background, but depending on an implementation detail like this is too clever in my opinion. I would add an “href = nil” to the else clause (or above the conditional), even though it is technically redundant, just to be explicit.