Conditional assignment

by Paweł Świątkowski
22 Aug 2014

The other day I was pair-programming with my boss and he wrote something along the lines of:

# a piece of code with no foo defined
foo = bar if some_condition
# ...
baz = foo

Whoa!’ I protested. ‘This ain’t gonna work as foo might be not defined.’ Soon enough he proved me wrong by typing in the console:

[1] pry(main)> x
NameError: undefined local variable or method `x' for main:Object
from (pry):1:in `__pry__'
[2] pry(main)> x = 1 if false
=> nil
[3] pry(main)> x
=> nil

I was a bit surprised and tried to explain this by assuming that somehow inline condition is binding stronger than assignment (we knew that if false; end returns nil). But then how would I explain what happens below?

[1] pry(main)> foo
NameError: undefined local variable or method `foo' for main:Object
from (pry):1:in `__pry__'
[2] pry(main)> if false
[2] pry(main)*   foo = 1
[2] pry(main)* end  
=> nil
[3] pry(main)> foo
=> nil

I would not. I had to turn to Google and I found this Stack Overflow answer. Long story short, it turns out that before actual interpretation Ruby parser scans the code and when it comes across an assignment to a not-yet-defined variable, it defines it with allocating memory for it. The actual assigment may never happen, because interpreter never reaches this particular line of code, but the variable is defined and has a value of nil nevertheless.

This is a weird side-effect and luckily happens only in given scope. See:

[1] pry(main)> def test
[1] pry(main)*   if false
[1] pry(main)*     y = 1
[1] pry(main)*   end  
[1] pry(main)*   y
[1] pry(main)* end  
=> :test
[2] pry(main)> def test2
[2] pry(main)*   y
[2] pry(main)* end  
=> :test2
[3] pry(main)> test
=> nil
[4] pry(main)> test2
NameError: undefined local variable or method `y' for main:Object
from (pry):8:in `test2'

It also seems to be a reason for famous undefined_var = undefined_var returning nil.

Achtung!

Before you start using it around, please note that this effect does not exist in Rubinius (I tested with version 2.2.10).

[1] pry(main)> if false
[1] pry(main)*   x = 1
[1] pry(main)* end  
=> nil
[2] pry(main)> x
NoMethodError: undefined method `x' on an instance of Object.
from kernel/delta/kernel.rb:78:in `x (method_missing)'
[1] pry(main)> x = 1 if false
=> nil
[2] pry(main)> x
NoMethodError: undefined method `x' on an instance of Object.
from kernel/delta/kernel.rb:78:in `x (method_missing)'

However, x = x when x has not been initialized still works.

JRuby behaves the same way as MRI.

end of the article

Tags: ruby weirdness

This article was written by me – Paweł Świątkowski – on 22 Aug 2014. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.