Ruby Instance Method & Conditional Local Variable Assignment with Same Name

Ruby instance method & conditional local variable assignment with same name

When a token can be interpreted as either a local variable or a method call, local variable has priority. The last numbers in the method definition is interpreted as a local variable. To make it interpreted as a method call, you need to make it unambiguous.

This is probably what you intended:

def get_numbers(condition)
return numbers = [3,5] if condition
numbers()
end

But this is very smelly code, and it should be like this:

def get_numbers(condition)
condition ? [3,5] : numbers
end
  • Does ruby creates [create] the variable numbers during runtime itself & [and] initializes [initialize it] to nil because of the if condition?

Yes. Whether or not the condition is satisfied, Ruby will parse everything, and if a local variable is not assigned because the condition is not satisfied, it will be initialized to nil.

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.

Method and variable name is the same

Try this:

puts hello()

To which object is a local variable assigned?

There are 4 kinds of variables in Ruby: local, instance, class and global.

def x
a = 1+2
end

a is a local variable; it is not an object but a place where a reference to the object 3 is stored. It also does not need an object to be "held" in (just a local lookup table/stack somewhere deep inside the guts of the ruby interpreter).

Same for the irb. (But see the answer from spickermann for more information on how irb internally transfers locals from one prompt to the next.)

The quote Everything in ruby is an object does not mean that literally everything is an object. For example, a method is not an object (although you can have an object of class Method which "points" to a method definition - the actual compiled code). A method call is not an object. A variable is not an object (though it points to an object).

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.

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

method's local variable with same name as another method

I think that the local variable is declared as soon as it's enunciated. In ruby the lookup is first to look for a local variable, if it exists it's used, and if not it looks for a method. This would mean that val = val declares the first val as local and the left-hand val then matches it (not sure about it I should check the ruby under microscope to be sure)

If you try

class A
def val
10
end

def test
back = []
x = val
back << x
val = x + 1
back << val
x = val
back << x
end
end

p A.new.test

then all is good, it prints [10, 11, 11] which means the first x = val calls the method, the second calls the local variable, presumably.

Why does conditional statement and assigning value in ruby fails if the if statement is at end of clause?

It fails because of the way the parser works.

From the parser's point of view the variable tmp2 exists from the point in the code at which it is first assigned until the point where it goes out of scope. For this it does not matter, when (or if) the assignment is actually executed, just when the parser sees the assignment (i.e. it depends on the assignments position in the code).

Edit: To expand on that bit:

The decision whether a name is a local variable or a method call is made by the parser. The parser makes that decision solely based on whether or not it has already seen an assignment to that variable. So when the parser sees tmp2 before seeing tmp2 = ..., it decides that here tmp2 refers to a method. When that part of the code is actually executed, it tries to call the method tmp2 which does not exist so you get the error.

Declaring variables in conditional

TLDR: Unfortunately, your examples are flawed, because you have chosen a name for your variable that clashes with existing method from core ruby.


As @SteveTurczyn mentioned a few minutes ago, if variable is not known before the line with the conditional, it's interpreted as a method call.

Let's explore some machine code, shall we? Important lines are commented.

puts "Am i a string? #{myvar}" if myvar = "Im a confused string"

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] myvar      
0000 trace            1                                               (   2)
0002 putstring        "Im a confused string"
0004 dup              
0005 setlocal_OP__WC__0 2
0007 branchunless     22
0009 putself          
0010 putobject        "Am i a string? "
0012 putself          
0013 opt_send_simple  <callinfo!mid:myvar, argc:0, FCALL|VCALL|ARGS_SKIP> # call method myvar
0015 tostring         
0016 concatstrings    2
0018 opt_send_simple  <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0020 leave            
0021 pop              
0022 putnil           
0023 leave            

And when variable is declared beforehands

myvar = "Im totally a string"
puts "Am i a string? #{myvar}" if myvar = "Im a confused string"

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] myvar      
0000 trace            1                                               (   1)
0002 putstring        "Im totally a string"
0004 setlocal_OP__WC__0 2
0006 trace            1                                               (   2)
0008 putstring        "Im a confused string"
0010 dup              
0011 setlocal_OP__WC__0 2
0013 branchunless     27
0015 putself          
0016 putobject        "Am i a string? "
0018 getlocal_OP__WC__0 2 # Read variable value
0020 tostring         
0021 concatstrings    2
0023 opt_send_simple  <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0025 leave            
0026 pop              
0027 putnil           
0028 leave            

Now, the problem with your code is that p is a method that exists. In case you didn't know about it, p foo is equivalent to puts foo.inspect. Similarly to puts, it accepts flexible number of arguments (even zero arguments) and returns nil.

puts "Am i a string? #{p}" if p = "Im a confused string"
^ call method `p` here

But

p = "foo" # Shadow existing method `p`
puts "Am i a string? #{p}" if p = "Im a confused string"
^ get local var
if you wanted to also call the method `p`, you'd have to just through some extra hoops
or just rename the variable.


Related Topics



Leave a reply



Submit