Why does Ruby seem to hoist variable declarations from inside a case statement even if that code path is not executed? [duplicate]
We define a function foo:
def foo(s)
case s
when'foo'
x = 3
puts x.inspect
when 'bar'
y = 4
puts y.inspect
end
puts x.inspect
puts y.inspect
end
We then call it as follows:
1.9.3p194 :017 > foo('foo')
in foo scope
3
in outer scope
3
nil
=> nil
1.9.3p194 :018 > foo('bar')
in bar scope
3
in outer scope
nil
3
=> nil
Why does the function not throw an error about an unregistered local variable in either case? In the first case, the variable y
seems like it should not exist, so you can't call inspect
on it in the outer scope; the same for x
in the second case.
Here's another similar example:
def test1
x = 5 if false
puts x.inspect
end
def test2
puts x.inspect
end
And then:
1.9.3p194 :028 > test1
nil
=> nil
1.9.3p194 :029 > test2
NameError: undefined local variable or method `x' for main:Object
What's going on here? It seems like Ruby is hoisting the variable declaration into the outer scope, but I wasn't aware that this is something Ruby does. (Searching for "ruby hoisting" only turns up results about JavaScript hoisting.)
Solution 1:
When the Ruby parser sees the sequence identifier, equal-sign, value, as in this expression
x = 1
it allocates space for a local variable called
x
. The creation of the variable—not the assignment of a value to it, but the internal creation of a variable—always takes place as a result of this kind of expression, even if the code isn’t executed! Consider this example:if false x = 1 end p x # Output: nil p y # Fatal Error: y is unknown
The assignment to
x
isn’t executed, because it’s wrapped in a failing conditional test. But the Ruby parser sees the sequencex = 1
, from which it deduces that the program involves a local variablex
. The parser doesn’t care whetherx
is ever assigned a value. Its job is just to scour the code for local variables for which space needs to be allocated. The result is thatx
inhabits a strange kind of variable limbo. It has been brought into being and initialized tonil
. In that respect, it differs from a variable that has no existence at all; as you can see in the example, examiningx
gives you the valuenil
, whereas trying to inspect the non-existent variabley
results in a fatal error. But althoughx
exists, it has not played any role in the program. It exists only as an artifact of the parsing process.
Well-Grounded Rubyist chapter 6.1.2
Solution 2:
The ruby parser goes through every lines and set to nil all variable =
. The code being actually executed or not does not matter.
See Why can I refer to a variable outside of an if/unless/case statement that never ran?