Ruby’s surprising handling of local variables

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?

4 thoughts on “Ruby’s surprising handling of local variables”

  1. 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.

  2. 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.

  3. Looks wrong to me but I was raised on C++ and Scheme, both of which have more fine grained scoping for variables than Java, Python, Ruby, Javascript have.

  4. BTW, Python does create the variable but leaves it undefined. You can test this by having a global named x and also a local named x inside an “if False”. Then try printing out x; it’ll raise an error:

    x = 1
    def f():
    if False:
    x = 3
    print x

    The same is true in Javascript, where x will be undefined:
    var x = 3;

    function f() {
    if (false) {
    var x = 5;
    }

    console.log(“” + x);
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">