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:
- The
name.nil?
statement would still refer to thename
instance method provided byattr_accessor
- When the Ruby parser sees the
name = "Unknown"
assignment line in the#say_name
method, it will consider any reference toname
used after the assignment to refer to the local variable - Therefore, even if the
Person
had aname
assigned to it on initialisation, thename
referenced in the final line of#say_name
method would benil
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?
- Was I wrong in my assumptions about
name.nil?
referencing thename
instance method? What is it referencing? - Is all this something related to being in the Pry environment?
- Something else I've missed?
For reference:
➜ [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
Edit
- The example code in this question is meant to be illustrative of the (I think) unexpected behaviour I'm seeing, and not in any way illustrative of actual good code.
- I know that this shadowing issue is easily avoided by re-naming the local variable to something else.
- Even with the shadowing, I know that it is still possible to avoid the problem by specifically invoking the method, rather than reference the local variable, with
self.name
orname()
.
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
?