Ruby Variable Assignment in a Conditional "If" Modifier

Ruby variable assignment in a conditional if modifier

Read it carefully :

Another commonly confusing case is when using a modifier if:

p a if a = 0.zero?

Rather than printing true you receive a NameError, “undefined local variable or method 'a'”. Since Ruby parses the bare a left of the if first and has not yet seen an assignment to a it assumes you wish to call a method. Ruby then sees the assignment to a and will assume you are referencing a local method.

The confusion comes from the out-of-order execution of the expression. First the local variable is assigned-to then you attempt to call a nonexistent method.

As you said - None return foo if (foo = bar.some_method) and return foo if (true && (foo = bar.some_method)) will work, I bet you, it wouldn't work, if you didn't define foo before this line.

Ruby 2.2.4 lexing an assignment in a conditional

Both lines won't work. And both lines will work. It is schrödinger expression :).

You can run it twice in a new repl:

a =  b if b = "test"
#=> NameError: undefined local variable or method `b' for main:Object
a = b if b = "test"
#=> "test"

Let's look deeper, open a new repl:

defined(b)
#=> nil
a = b if b = "test"
#=> NameError: undefined local variable or method `b' for main:Object
defined(b)
#=> local-variable
b
#=> "test"
a = b if b = "test"
#=> "test"

So actually Ruby has evaluated b = "test" part and defined this variable in current scope. Both expressions a = b and if b = "test" were executed. More than it, if statement is executed before assignment statement:

c = p("assignment") && b if b = p("if") && "test"
#=> "if"
#=> "assignment"
#=> NameError: undefined local variable or method `b' for main:Object

But b variable was not defined in scope of assignment statement when it was evaluated first time. And on the second approach it was already defined so you received correct result.

So, Never do assignments in this way

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.

Why is assignment treated differently in this single line conditional?

Oddly enough, this works fine in Rubinius:

Welcome to IRB. You are using rubinius 1.2.4dev (1.8.7 7ae451a1 yyyy-mm-dd JI)
>> (puts x) if (x = 0) #=> nil
0

I'm inclined to say it's a weird parsing bug in MRI.

Why does ruby define variables even if it never executes the variable assignment code?

Some of the docs explain how variables are created; the explanation as I understand it is that's just how the parser works:

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

You can see other examples of this:

b = true if false # b is nil
"test" || c = true # c is nil

And others it doesn't get assigned:

puts d if false # d generates a NameError

Ruby variable assignment for not evaluated lines

Before your Ruby code can be run, it must first be parsed, and it's at this stage that the behavior you're experiencing originates.

As the parser scans through the code, whenever it encounters a declaration (foo = 'something') it allocates space for that variable by setting its value to nil. Whether that variable declaration is actually executed in the context of your code is irrelevant. For example:

if false
foo = 42
end

p foo
#=> nil

In the above code's logic foo is never declared, however it's space in memory is recognized and allocated for by Ruby when the code is parsed out.

Hope this helps!

Assign variable only if not nil

The style I generally see looks like this:

@obj.items_per_page = many_items if many_items

This uses the inline conditional, while avoiding negative or double-negative conditions.

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

Ruby if vs end of the line if behave differently?

This is a very good question. It has to do with the scoping of variables in Ruby.

Here is a post by Matz on the Ruby bug tracker about this:

local variable scope determined up to down, left to right. So a local variable first assigned in the condition of if modifier is not effective in the left side if body. It's a spec.

Variable scope and order of parsing vs. operations: Assignment in an if

It only happens when you try to assign a literal value, if you call a function it works.

def foo(a)
a
end

p 'not shown' if(value = foo(false))
p 'shown' if(value = foo(true))

# This outputs a Warning in IRB
p 'shown' if(value = false)
(irb):2: warning: found = in conditional, should be ==

If you turn on debugging (-d) you will see a warning about an used variable value

warning: assigned but unused variable - value

This "works" because the statement does evaluate to true as far as if is concerned, allowing the code preceeding it to run.

What is happening here is that if() when used as a modifier has it's own binding scope, or context. So the assignment is never seen outside of the if, and therefore makes no sense to perform. This is different than if the control structure because the block that the if statement takes is also within the same scope as the assignment, whereas the line that preceeded the if modifier is not within the scope of the if.

In other words, these are not equivelant.

if a = some(value)
puts a
end

puts a if(a = some(value))

The former having puts a within the scope of the if, the latter having puts a outside the scope, and therefore having different bindings(what ruby calls context).

Ruby Order of Operations



Related Topics



Leave a reply



Submit