Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't my variable getting defined, and ending this loop?

Tags:

ruby

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>
like image 620
vt102 Avatar asked Jul 02 '14 21:07

vt102


People also ask

What happens when a variable value type data goes out of its scope?

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.

Are variables in for loop local?

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.


2 Answers

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.

like image 82
tadman Avatar answered Sep 28 '22 01:09

tadman


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
like image 37
Matt Avatar answered Sep 28 '22 02:09

Matt