How Is a Local Variable Created Even When If Condition Evaluates to False in Ruby

How is a local variable created even when IF condition evaluates to false in Ruby?

Local variables in ruby are created during parsing/compilation of code (not execution). They are lexically scoped, so a local variable is not visible before the line where it's assigned to.

defined?(foo) # => nil
if false
defined?(foo) # =>
foo = 'blah'
defined?(foo) # =>
end

defined?(foo) # => "local-variable"
foo # => nil

defined?(foo) lines inside of if return nothing, because they didn't run. The assignment wasn't executed as well. However, the compiler saw the assignment to local variable and created one (with default value of nil).

This behaviour explains the trick from WAT talk:

a = a # => nil

Even though variable a doesn't exist, it is created (and set to nil) right before this line, simply because there is an assignment expression in the code (target of which is yet unknown local variable). So by the time the right hand side of this expression is evaluated, a exists.

Why is a variable declared when it appears in if false ?

Ruby handles assignments at the parser level. From the documentation:

The local variable is created when the parser encounters the
assignment, not when the assignment occurs:

a = 0 if false # does not assign to a

p local_variables # prints [:a]

p a # prints nil

Confusion with the assignment operation inside a falsy `if` block

In Ruby, local variables are defined by the parser when it first encounters an assignment, and are then in scope from that point on.

Here's a little demonstration:

foo # NameError: undefined local variable or method `foo' for main:Object

if false
foo = 42
end

foo # => nil

As you can see, the local variable does exist on line 7 even though the assignment on line 4 was never executed. It was, however, parsed and that's why the local variable foo exists. But because the assignment was never executed, the variable is uninitialized and thus evaluates to nil and not 42.

In Ruby, most uninitialized or even non-existing variables evaluate to nil. This is true for local variables, instance variables and global variables:

defined? foo       #=> nil
local_variables #=> []
if false
foo = 42
end
defined? foo #=> 'local-variable'
local_variables #=> [:foo]
foo #=> nil
foo.nil? #=> true

defined? @bar #=> nil
instance_variables #=> []
@bar #=> nil
@bar.nil? #=> true
# warning: instance variable @bar not initialized

defined? $baz #=> nil
$baz #=> nil
# warning: global variable `$baz' not initialized
$baz.nil? #=> true
# warning: global variable `$baz' not initialized

It is, however, not true for class hierarchy variables and constants:

defined? @@wah     #=> nil
@@wah
# NameError: uninitialized class variable @@wah in Object

defined? QUUX #=> nil
QUUX
# NameError: uninitialized constant Object::QUUX

This is a red herring:

defined? fnord     #=> nil
local_variables #=> []
fnord
# NameError: undefined local variable or method `fnord' for main:Object

The reason why you get an error here is not that unitialized local variables don't evaluate to nil, it is that fnord is ambiguous: it could be either an argument-less message send to the default receiver (i.e. equivalent to self.fnord()) or an access to the local variable fnord.

In order to disambiguate that, you need to add a receiver or an argument list (even if empty) to tell Ruby that it is a message send:

self.fnord
# NoMethodError: undefined method `fnord' for main:Object
fnord()
# NoMethodError: undefined method `fnord' for main:Object

or make sure that the parser (not the evaluator) parses (not executes) an assignment before the usage, to tell Ruby that it is a local variable:

if false
fnord = 42
end
fnord #=> nil

And, of course, nil is an object (it is the only instance of class NilClass) and thus has an object_id method.

Local variables within if statement

Referencing a inside the if has the effect of declaring it as a variable if there is no method a= defined for the object.

Since Ruby does not require methods to be called using the same syntax as referencing a variable or assigning to one, it needs to make an assessment as to the nature of the token in question. If it could be a method call because a method with that name has been defined, then it will be interpreted as such. If no such method exists at the time the source is compiled, then it will be a variable by default.

Does the || operator evaluate the second argument even if the first argument is true?

This happens because the ruby interpreter defines a variable when it sees an assignment to it (but before it executes the actual line of code). You can read more about it in this answer.

Boolean OR (||) expression will evaluate to the value of left hand expression if it is not nil and not false, else || will evaluate to the value of right hand expression.

In your example the ruby interpreter sees an assignment to a and rr (but it doesn't execute this line yet), and initializes (defines, creates) a and rr with nil. Then it executes the || expression. In this || expression, a is assigned to 10 and 10 is returned. r=20 is not evaluated, and rr is not changed (it is still nil). This is why in the next line rr is nil.

Why can I refer to a variable outside of an if/unless/case statement that never ran?

It's because of how the Ruby parser works. Variables are defined by the parser, which walks through the code line-by-line, regardless of whether it will actually be executed.

Once the parser sees x =, it defines the local variable x (with value nil) henceforth in the current scope. Since if/unless/case/for/while do not create a new scope, x is defined and available outside the code block. And since the inner block is never evaluated as the conditional is false, x is not assigned to (and is thus nil).

Here's a similar example:

defined?(x) and x = 0
x #=> nil

Note that this is a rather high-level overview of what happens, and isn't necessarily exactly how the parser works.

Variable defined despite condition should prevent it

This is expected behavior in Ruby. Quote from the Ruby docs:

The local variable is created when the parser encounters the assignment, not when the assignment occurs:

a = 0 if false # does not assign to a

p local_variables # prints [:a]

p a # prints nil

Why is `a = a` `nil` in Ruby?

Ruby interpreter initializes a local variable with nil when it sees an assignment to it. It initializes the local variable before it executes the assignment expression or even when the assignment is not reachable (as in the example below). This means your code initializes a with nil and then the expression a = nil will evaluate to the right hand value.

a = 1 if false
a.nil? # => true

The first assignment expression is not executed, but a is initialized with nil.

You can find this behaviour documented in the Ruby assignment documentation.

Variable in else condition assumed nil value


The local variable is created when the parser encounters the
assignment, not when the assignment occurs:

=> foo
# NameError: undefined local variable or method `foo' for main:Object
=> if false
=> foo = "bar" # does not assign to foo
=> end
=> nil
=> foo
=> nil

Why does `x =! 5` return false?

This is because x =! 5 is being interpreted as x = (!5) (! has higer precedence than =). In Ruby every object is true except nil and false. 5 has truthy value which you are negating using the operator !. So false as result is being assigned to the local variable x.

! Called Logical NOT Operator - is used to reverse the logical state of its operand. If a condition is true, then Logical NOT operator will make false.



Related Topics



Leave a reply



Submit