Ruby statement modifers behave differently than conditional statements
If you’re a Ruby guy or gal, you know about statement modifiers. They are lovely little bits of syntax that let us do stuff like:
raise "You're an idiot" if params.nil? call_some_awesome_method unless it_was_already_called
Which, we’ve all been told, is the exact same thing as doing:
if params.nil? raise "You're an idiot" end unless it_was_already_called call_some_awesome_method end
Well, as it turns out, this isn’t true. Statement modifiers in Ruby behave differently than their conditional brethren. Something that caused me quite a bit of pain a couple weeks ago. Take the following bit of code. We’ll go through it line by line.
ra:~$ irb irb(main):001:0> a NameError: undefined local variable or method `a' for main:Object from (irb):1 irb(main):002:0> defined? a => nil irb(main):003:0> a NameError: undefined local variable or method `a' for main:Object from (irb):3 irb(main):004:0> a = 5 unless defined? a => nil irb(main):005:0> a => nil irb(main):006:0> defined? a => "local-variable" irb(main):007:0>
- crack open an irb console and try to access the variable a. It hasn’t been defined yet, so irb appropriately complains.
- use the defined? keyword to check what the interpreter currently thinks of that variable. nil is returned. The interpreter has never seen it. Undefined as expected.
- lets check a again to see if calling defined? on it did antything. Nope. Still undefined. So far so good.
- Now, I write some code that I think means “Hey Ruby, if you have never seen a before, then set it equal to 5.
- … a is nil… wtf?
- ?
Thought I knew Ruby. Let’s rewrite this using conditionals instead of modifiers.
irb(main):007:0> b NameError: undefined local variable or method `b' for main:Object from (irb):7 irb(main):008:0> defined? b => nil irb(main):009:0> unless defined? b irb(main):010:1> b = 5 irb(main):011:1> end => 5 irb(main):012:0> b => 5 irb(main):013:0> defined? b => "local-variable" irb(main):014:0>
Which is what you’d expect. So what’s going on here? My first thought was that maybe there was an operator binding precedence thing going on. Like maybe it was evaluating: a = (5 unless defined?(a)). But even that should assign 5 to a. Then I tried (a = 5) unless defined?(a). No luck. Same behavior. a gets assigned nil.
I think the explanation goes something like this: In the first example, the interpreter sees a and realizes right away that it’s an lvalue. As such, it adds a to the local variable list. Then, it continues on and sees an assignment operation (=), an expression (5) and a statement modifier (unless defined? a). It delays evaluating the expression and executing the assignment because of the statement modifier. Once it evaluates that modifier, it decides to not execute the expression or assignment.
So I think about this for a while wondering how I could test the theory and it dawns on me that there is a much simpler way to illustrate this problem:
irb(main):001:0> a NameError: undefined local variable or method `a' for main:Object from (irb):1 irb(main):002:0> a = 5 unless (res = defined?(a)) => nil irb(main):003:0> a => nil irb(main):004:0> res => "local-variable" irb(main):005:0>
I’m not sure if I’m missing something super obvious, something super obscure, or if this is a bug in Ruby. But I know that res should be nil and a == 5.
Just something to keep in mind down the road. Statement modifiers behave differently than conditional statements.
