Defined? behaviour

I’m trying this code:

var = true

if var
   puts "var"
else
   other = true
end

puts defined?(other)

Result is this:

$ ruby example.rb
var
local-variable
$

Why is “other” being defined if its part of the code is never reached?
Different result is obtained if used “@other

Short answer: that’s just the way it is.

Longer answer:

if false
x = ‘hello’
end

puts x #=>nil
puts @x #=>nil
puts $x #=>nil

puts y #exception: undefined local variable or method

First, the ruby parser examines the code for things like syntax errors.
For local variables, whenever the ruby parser sees var =, it creates the
variable; then when the code executes, an assignment either takes
place or it doesn’t. By default, instance variables and global
variables have an initial value of nil.

But if you use “@other” in my example, “defined?(@other)” will return
nil, and not “instance-variable”, while “defined?(other)” returns
“local-variable”.

Why?

But if you use “@other” in my example, “defined?(@other)” will
return nil, and not “instance-variable”, while “defined?(other)”
returns “local-variable”.

@other” denotes an instance variable.

“other” would denote a local variable.

The Ruby parser does not entirely check whether other is a “real” local
variable or not. I think the reason it is not doing this is because it
is not necessary.

It simply assumes that, by seeing something like “foo =” the ruby
program wants to create a local variable called foo and assign it some
value.

That is why that local variable is “known”.

And by using defined?() you are asking precisely that.

What behaviour did you expect to see instead?

Also, you stated that “if you use “@other” in my example,
“defined?(@other)” will return nil”

Yes because @other is not assigned a value!
Your condition is not checked.

If you write it like this:

var = true # => true

if var
@other = true
else
puts “var”
end

puts defined?(@other) # instance-variable

This is returned.

PS: Perhaps it may help you if you think of local variables as
“meaningless throwaway variables”. If you need more persistent
variables, just use a proper class and @foo instance variables. My
favourite local throwaway variable name is _ by the way - not that it
matters. But it simply isn’t important so I do not even want to give it
a name like “a” or “x” at all.

Javier 12 wrote in post #1008467:

But if you use “@other” in my example, “defined?(@other)” will return
nil, and not “instance-variable”, while “defined?(other)” returns
“local-variable”.

Why?

You’ve got a good point.
The behavior is confusing and inconsistent with other variables.

When Ruby encounters “local = …”, whether or not that
expression is executed, its value is set to nil.
Ruby considers it defined.

instance, class, and global variables are defined when encountered at
runtime. Locals are the exception:

if false
local = 1
end

p local_variables # => [ :local ]
p local # => nil

Armed with this knowledge, you can use local.nil? to replace your call
to defined?().

HTH,
Rob

Robert G. wrote in post #1008764:

Javier 12 wrote in post #1008467:

But if you use “@other” in my example, “defined?(@other)” will return
nil, and not “instance-variable”, while “defined?(other)” returns
“local-variable”.

Why?

You’ve got a good point.
The behavior is confusing and inconsistent with other variables.

But absolutely essential, because this is how ruby solves the local
variable / method call ambiguity.

By this I mean: because ruby doesn’t require parentheses on a method
call, a bare word like “foo” could either be a method call with no
arguments, or a local variable access.

puts foo
      ^
    what's this?

The distinction is made at parse time. If, when the syntax tree is being
built, there’s an expression of the form “foo = xxx” earlier in this
scope, then from that point onwards a bareword “foo” is considered a
local variable. Its slot on the stack frame is initialised to nil.

If necessary, you can force it the other way: self.foo or foo() are both
method calls, even if there is a foo local variable in this scope.

If this were not done at parse time, even a simple statement like

a = a + 1

would be horrendously expensive to execute. You would have to test
whether the ‘a’ on the right-hand side existed as a local variable at
that instant, and if not, try to call a() instead.

So the upshot is:

def foo
“Function”
end

def bar
“Another function”
end

if false
foo = 123
end

puts foo # nil
puts foo() # “Function”
puts self.foo # “Function”
puts bar # “Another function”
bar = 456
puts bar # 456
puts baz # undefined local variable or method baz' puts baz() # undefined methodbaz’

Note the error message in the penultimate line. The error might be that
you forgot to assign to variable baz earlier in the method, or you
forgot to define a method baz(). Ruby doesn’t know which you intended.

Yeah, the pay off is worth it :slight_smile: