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 does it mean for a local variable to shadow a field? A method may have a local variable with the same name as an instance field. This is called shadowing. The local variable will hide the value of the instance field. Shadowing is discouraged and local variable names should not be the same as instance field names.
In computer programming, variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope. At the level of identifiers (names, rather than variables), this is known as name masking.
How to avoid variable shadowing? To modify a global variable, and avoid variable shadowing python provides global keyword which tells python to use the global version of the variable, instead of creating a new locally scoped variable.
Shadowing in Java is the practice of using variables in overlapping scopes with the same name where the variable in low-level scope overrides the variable of high-level scope. Here the variable at high-level scope is shadowed by the low-level scope variable.
Once Ruby has decided that name
is a variable and not a method call that information applies to the totality of the scope it appears within. In this case it's taking it to mean the whole method. The trouble is if you have a method and a variable with the same name the variable only seems to take hold on the line where the variable has been potentially assigned to and this re-interpretation affects all subsequent lines within that method.
Unlike in other languages where method calls are made clear either by some kind of prefix, suffix or other indicator, in Ruby name
the variable and name
the method call look identical in code and the only difference is how they're interpreted at "compile" time proior to execution.
So what's happening here is a little confusing and subtle but you can see how name
is being interpreted with local_variables
:
def say_name_local_variable
p defined?(name) # => "method"
p local_variables # => [:name] so Ruby's aware of the variable already
if name.nil? # <- Method call
name = "Unknown" # ** From this point on name refers to the variable
end # even if this block never runs.
p defined?(name) # => "local-variable"
p name # <- Variable value
puts "My name is #{name.inspect}"
end
I'm quite surprised that, given how obnoxiously particular Ruby can be with the -w
flag enabled, that this particular situation generates no warnings at all. This is likely something the'll have to emit a warning for, a strange partial shadowing of methods with variables.
To avoid method ambiguity you'll need to prefix it to force it to be a method call:
def say_name
name = self.name || 'Unknown'
puts "My name is #{name.inspect}"
end
One thing to note here is that in Ruby there are only two logically false values, literal nil
and false
. Everything else, including empty strings, 0
, empty arrays and hashes, or objects of any kind are logically true. That means unless there's a chance name
is valid as literal false
then ||
is fine for defaults.
Using nil?
is only necessary when you're trying to distinguish between nil
and false
, a situation that might arise if you have a three-state checkbox, checked, unchecked, or no answer given yet.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With