.

Coffee Powered

code and content

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 foobar because it’s referenced, set it to nil
  3. If the condition is true, set foobar to 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.

8 Comments

  1. March 2, 2009 at 3:23 pm | Permalink

    This not as much about the trailing condition but about the defined? method which itself is pretty tricky and should be avoided. I myself spent a sleepless night trying to check if something is defined and write the code in a way that would not accidentally define it before the actual defined? is evaluated.

  2. March 2, 2009 at 3:26 pm | Permalink

    Well, it’s a combination of the two, really. The real gotcha is that Ruby initializes variables as they’re parsed, not as the code is run – I wasn’t expecting that at all.

  3. Scott
    March 2, 2009 at 3:26 pm | Permalink

    You can just use:
    foobar ||= true
    which will set foobar to true if it’s undefined, nil, or false, but otherwise leave it’s value the same.
    It’s just the same as:
    foobar = foobar || true

    Still, it’s good to remember these tricky order of operations things.

  4. March 2, 2009 at 3:30 pm | Permalink

    @Scott – yeah, I usually do. The problem here was that “false” is a valid initialization value for the variable I was interested in, and I didn’t want to clobber that if it was already set.

  5. March 3, 2009 at 10:17 am | Permalink

    Interesting stuff, I’ve come across the same problem a few times and never been satisfied, we want a false (and/or nil) preserving equivalent of ||=

    Surely there’s a cleaner or more idiomatic way of approaching this than using line separators?

    Trashing around in IRB testing the behaviour I tried:

    irb(main):002:0> foo = defined?(foo) || true
    => “local-variable”

    which was another result I wasn’t expecting…

  6. March 3, 2009 at 10:20 am | Permalink

    In fact

    irb(main):002:0> bar = defined?(bar)
    => “local-variable”

    Shows the behaviour more clearly. (MRI 1.8.7)

  7. March 3, 2009 at 4:20 pm | Permalink

    The more I think about it, the less convinced I am that a more idiomatic solution exists, because anything that does a left-hand assignment is going to initialize the variable to be assigned to before the right-hand side of the expression is evaluated.

    The only way I could see this working would be if there were some kind of right-hand assignment operator in ruby, which I’m fairly certain there isn’t.

  8. March 4, 2009 at 7:01 am | Permalink

    Trailing if/unless statements come from Perl and these same problems are well known there when it comes to declaring variables ;-)

    However this does allow declaration of state variables….

    my $persist if 0;

    This peculiarity can now be replaced in Perl 5.10 with the much nicer…

    state $persist;

    “Perl Best Practises” only recommends u use them with next, last & redo (and in fact not use “unless” at all!). I don’t go that far but I certainly make sure I never use variable declaration with them!

    /I3az/

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*