I recently read a blog post about Ruby's behaviours with regards to a local variable shadowing a method (different to, say, a block variable shadowing a method local variable, which is also talked about in this StackOverflow thread), and I found some behaviour that I don't quite understand.
Ruby's documentation says that:
[V]ariable names and method names are nearly identical. If you have not assigned to one of these ambiguous names ruby will assume you wish to call a method. Once you have assigned to the name ruby will assume you wish to reference a local variable.
So, given the following example class
# person.rb
class Person
attr_accessor :name
def initialize(name = nil)
@name = name
end
def say_name
if name.nil?
name = "Unknown"
end
puts "My name is #{name.inspect}"
end
end
and given what I now know from reading the information from the links above, I would expect the following:
name.nil?
statement would still refer to the name
instance method provided by attr_accessor
name = "Unknown"
assignment line in the #say_name
method, it will consider any reference to name
used after the assignment to refer to the local variablePerson
had a name
assigned to it on initialisation, the name
referenced in the final line of #say_name
method would be nil
And it looks like this can be confirmed in an irb
console:
irb(main):001:0> require "./person.rb"
true
# `name.nil?` using instance method fails,
# `name` local variable not assigned
irb(main):002:0> Person.new("Paul").say_name
My name is nil
nil
# `name.nil?` using instance method succeeds
# as no name given on initialisation,
# `name` local variable gets assigned
irb(main):003:0> Person.new.say_name
My name is "Unknown"
nil
However, if I do some inline debugging and use Pry to attempt to trace how the referencing of name
changes, I get the following:
irb(main):002:0> Person.new("Paul").say_name
From: /Users/paul/person.rb @ line 13 Person#say_name:
10: def say_name
11: binding.pry
12:
=> 13: p name
14: if name.nil?
15: name = "Unknown"
16: end
17:
18: puts "My name is #{name.inspect}"
19: end
[1] pry(#<Person>)> next
"Paul"
Okay, that makes sense as I'm assuming name
is referring to the instance method. So, let's check the value of name
directly...
From: /Users/paul/person.rb @ line 14 Person#say_name:
10: def say_name
11: binding.pry
12:
13: p name
=> 14: if name.nil?
15: name = "Unknown"
16: end
17:
18: puts "My name is #{name.inspect}"
19: end
[2] pry(#<Person>)> name
nil
Err... that was unexpected at this point. I'm currently looking at a reference to name
above the assignment line, so I would have thought it would still reference the instance method and not the local variable, so now I'm confused... I guess somehow the name = "Unknown"
assignment will run, then...?
[3] pry(#<Person>)> exit
My name is nil
nil
Nope, same return value as before. So, what is going on here?
name.nil?
referencing the name
instance method? What is it referencing?For reference:
➜ [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
self.name
or name()
.Playing around with this further, I'm starting to think it's perhaps an issue around Pry's environment. When running Person.new("Paul").say_name
:
From: /Users/paul/person.rb @ line 13 Person#say_name:
10: def say_name
11: binding.pry
12:
=> 13: p name
14: if name.nil?
15: name = "Unknown"
16: end
17:
18: puts "My name is #{name.inspect}"
19: end
At this point, the p
statement hasn't run yet, so let's see what Pry says the value of name
is:
[1] pry(#<Person>)> name
nil
This is unexpected given that Ruby's documentation says that since no assignment has been made yet, the method call should be invoked. Let's now let the p
statement run...
[2] pry(#<Person>)> next
"Paul"
...and the value of the method name
is returned, which is expected.
So, what is Pry seeing here? Is it modifying the scope somehow? Why is it that when Pry runs name
it gives a different return value to when Ruby itself runs name
?
What looks like inconsistent return values for name
during runtime and while debugging doesn't seem to related to Pry, but more about binding
itself encapsulating the entire execution context of a method, versus the progressive change in what shadowed variables reference at runtime. To build on the example method with some more debugging code:
def say_name
puts "--- Before assignment of name: ---"
puts "defined?(name) : #{defined?(name).inspect}"
puts "binding.local_variable_defined?(:name) : #{binding.local_variable_defined?(:name).inspect}"
puts "local_variables : #{local_variables.inspect}"
puts "binding.local_variables : #{binding.local_variables.inspect}"
puts "name : #{name.inspect}"
puts "binding.eval('name') : #{binding.eval('name').inspect}"
if name.nil?
name = "Unknown"
end
puts "--- After assignment of name: ---"
puts "defined?(name) : #{defined?(name).inspect}"
puts "binding.local_variable_defined?(:name) : #{binding.local_variable_defined?(:name).inspect}"
puts "local_variables : #{local_variables.inspect}"
puts "binding.local_variables : #{binding.local_variables.inspect}"
puts "name : #{name.inspect}"
puts "binding.eval('name') : #{binding.eval('name').inspect}"
puts "My name is #{name.inspect}"
end
Now, running Person.new("Paul").say_name
outputs:
--- Before assignment of name: ---
defined?(name) : "method"
binding.local_variable_defined?(:name) : true
local_variables : [:name]
binding.local_variables : [:name]
name : "Paul"
binding.eval('name') : nil
--- After assignment of name: ---
defined?(name) : "local-variable"
binding.local_variable_defined?(:name) : true
local_variables : [:name]
binding.local_variables : [:name]
name : nil
binding.eval('name') : nil
My name is nil
which shows that binding
never references the method call of name
and only ever the eventually-assigned name
variable.