Chris Heald bio photo

Chris Heald

Chief Architect for Mashable. Rubyist, husband, father, and all around tall guy.

Syntactic sugar will occassionally kick your puppies.

Ruby's awesome. It has sweet, concise syntax that makes for clean, readable code. One of these constructs is the trailing condition. In most languages where you might have to write something like:

if foo then
    do_stuff
end

Ruby will let you clean that up with:

do_stuff if foo

This works just nearly all the time, but I ran into an odd problem today, where the trailing conditions were producing behavior I didn't want.

>> foobar
NameError: undefined local variable or method `foobar' for #<Object:0x92bc998>
        from (irb#1):2
>> foobar = true unless defined?(foobar)
=> nil
>> foobar
=> nil
>> unless defined?(foobar); foobar = true; end
=> true
>> foobar
=> true

Wait, what? Using the trailing conditional changes the order in which Ruby parses the statement, resulting in something like the following operations:

  1. Define foobar because it's referenced, set it to nil
  2. Parse the unless conditional
  3. If the condition is true, set foobar to true

The kicker here is that because foobar's assignment is the first thing parsed, it's always initialized before you ever get to the defined? statement. So instead, we run the second piece of code:

unless defined?(foobar); foobar = true; end

This runs something like the following:

  1. Parse the unless condition.
  2. Define foobarbecause it's referenced, set it to nil
  3. If the condition is true, set foobarto true

Obviously this is the desired behavior. Several lessons here:

  • Ruby initializes variables when they are parsed, not when the code path that contains them is run (in fact, it'll even initialize variables that are in unreachable code paths!)
  • if condition then do_stuff end is not always the same as do_stuff if condition

It's a bit of an edge case, but it's an edge case that had me baffled. Hopefully this post saves you some frustration.