I ran across this issue. I have a work around, so I'm specifically asking WHY this doesn't work? Why doesn't the variable get defined in the first time through the loop, and then exit the loop?
Assuming it's the scope, which I'm convinced it is, then why is foo defined after breaking the loop? Am I just seeing an artifact of irb here?
voin0017:[/home/acowell/src/local/goldrhel] ruby -v
ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-linux]
voin0017:[/home/acowell/src/local/goldrhel] irb
irb(main):001:0> while defined?(foo).nil? ; foo = 1 ; end
^CIRB::Abort: abort then interrupt!
from (irb):1:in `call'
from (irb):1
from /opt/chef/embedded/bin/irb:12:in `<main>'
irb(main):002:0> p foo
1
=> 1
irb(main):003:0>
Nothing physical happens. A typical implementation will allocate enough space in the program stack to store all variables at the deepest level of block nesting in the current function. This space is typically allocated in the stack in one shot at the function startup and released back at the function exit.
In Python, on the other hand, variables declared in if-statements, for-loop blocks, and while-loop blocks are not local variables, and stay in scope outside of the block.
The defined?
method does not test if a value is defined for a variable, but instead if that variable is defined within the scope you're testing it in.
The code you've written here is basically nonsensical:
while (defined?(foo).nil?)
foo = 1
end
The foo
variable is never "defined" outside of the context of the while
block so it spins forever waiting for that to happen. You end up defining it repeatedly inside the context of the while
loop, but that defined?
check does not test that. It is getting defined, just not where you expected it to be.
If you add a little code to see what's going on you get this:
puts defined?(foo).inspect
# => nil
while (true)
foo = 1
puts defined?(foo).inspect
# => "local-variable"
break
end
# Once the while completes, the variable is defined outside that scope.
puts defined?(foo).inspect
# => "local-variable"
The general pattern you're going in idiomatic Ruby code looks more like this
foo = nil
while (!foo)
foo = 1
end
As a note, it's highly unusual to see defined?
used in Ruby applications as variables are either used, or not used. It's in situations where you have some automatic code generation and block rebinding going on that you want to test local variables before using them. The most common case for this rare behaviour is within Rails partials where you may have passed in options via the :local
argument. These appear as local variables if defined, but may not exist, so you need to test to be sure.
Whether or not a variable is defined depends upon not only the scope, but also on its location within the ruby script. That is, a variable is defined only if it has been defined previously within the parse, not the execution.
Here's an example:
begin
puts "Is foo defined? #{defined?(foo).inspect}" # foo is never defined here
foo ||= 1
puts "but now foo is #{foo}" # foo is always defined here
foo += 1
end while foo <= 3
Output:
Is foo defined? nil
but now foo is 1
Is foo defined? nil
but now foo is 2
Is foo defined? nil
but now foo is 3
Because foo
has not been defined previously within the script on the first line of the loop, it is undefined at that point, and remains undefined, even if it is assigned to and the same line is returned to at a later point during execution.
This is why foo
in the while
condition of the question is always undefined:
while defined?(foo).nil? # foo is always undefined
foo = 1
end
and will loop forever. In contrast, this loop only executes once:
begin
foo = 1
end while defined?(foo).nil? # foo is defined
because foo
is assigned to previously in the parse.
Edit:
Only loops that require a block seem to isolate its local variables from living outside of it. E.g. loop
, upto
, each
, inject
, map
, times
, etc. These all require use of the keyword do
and end
, or curly braces, which delimit the block. In contrast, while
, until
, and for
do not, and so variables defined within them continue to live outside of them. This is demonstrated here:
while true
foo_while = 1
break
end
puts "foo_while: #{defined?(foo_while).inspect}"
until false
foo_until = 1
break
end
puts "foo_until: #{defined?(foo_until).inspect}"
for i in 0..2
foo_for = 1
break
end
puts "foo_for: #{defined?(foo_for).inspect}"
loop do
foo_loop = 1
break
end
puts "foo_loop: #{defined?(foo_loop).inspect}"
1.upto(2) do |i|
foo_upto = 1
break
end
puts "foo_upto: #{defined?(foo_upto).inspect}"
[1,2,3].each do |i|
foo_each = 1
break
end
puts "foo_each: #{defined?(foo_each).inspect}"
[1,2,3].inject do |i,j|
foo_inject = 1
break
end
puts "foo_inject: #{defined?(foo_inject).inspect}"
[1,2,3].map do |i|
foo_map = 1
break
end
puts "foo_map: #{defined?(foo_map).inspect}"
3.times do
foo_times = 1
break
end
puts "foo_times: #{defined?(foo_times).inspect}"
Output:
foo_while: "local-variable"
foo_until: "local-variable"
foo_for: "local-variable"
foo_loop: nil
foo_upto: nil
foo_each: nil
foo_inject: nil
foo_map: nil
foo_times: nil
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With