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:
- Define
foobarbecause it’s referenced, set it tonil - Parse the
unlessconditional - If the condition is true, set
foobartotrue
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:
- Parse the
unlesscondition. - Define
foobarbecause it’s referenced, set it tonil - If the condition is true, set
foobartotrue
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 endis not always the same asdo_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
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.
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.
You can just use:
foobar ||= truewhich 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 || trueStill, it’s good to remember these tricky order of operations things.
@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.
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…
In fact
irb(main):002:0> bar = defined?(bar)
=> “local-variable”
Shows the behaviour more clearly. (MRI 1.8.7)
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.
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/