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 thenameinstance method provided byattr_accessor - When the Ruby parser sees the
name = "Unknown"assignment line in the#say_namemethod, it will consider any reference tonameused after the assignment to refer to the local variable - Therefore, even if the
Personhad anameassigned to it on initialisation, thenamereferenced in the final line of#say_namemethod 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 thenameinstance 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.nameorname().
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?