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
Cannot Click HTML Element with Watir
What Is the &: of &:Afunction Doing
Understanding the Fibonacci Sequence
How to Use Ruby for Shell Scripting
How to Find Where Gem Files Are Installed
Bundler VS Rvm VS Gems VS Rubygems VS Gemsets VS System Ruby
Rmagick Installation: Can't Find Magickwand.H
How to Step Out of a Loop with Ruby Pry
Checking If a Variable Is Not Nil and Not Zero in Ruby
Rails: Convert Utc Datetime to Another Time Zone
Convert String to Symbol-Able in Ruby
Ruby Array to String Conversion