5

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 the name instance method provided by attr_accessor
  • When the Ruby parser sees the 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 variable
  • Therefore, even if the Person 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?

  • Was I wrong in my assumptions about name.nil? referencing the name 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 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?

Paul Fioravanti
  • 16,423
  • 7
  • 71
  • 122
  • I just wanted to say the obvious: the whole trouble can easily be avoided by using a different variable name, e.g. `display_name = name || "Unknown"`. – Stefan Oct 06 '17 at 08:09
  • Yes, correct. The code in the question is written the way it is strictly to illustrate the issue that I was seeing. – Paul Fioravanti Oct 06 '17 at 08:32

2 Answers2

5

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.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • 1
    Thanks for the answer. With regards to Ruby having "decided that `name` is a variable and not a method call that information applies to the totality of the scope it appears within" (I read that somewhere too about applying the variable value to the totality of scope), it would seem that isn't happening here in the scope of the `#say_name` method, otherwise `Person.new("Paul").say_name` and `Person.new.say_name` would print out the same messages (ie `name.nil?` would fail both times). What am I missing? – Paul Fioravanti Oct 06 '17 at 03:13
  • That might be due to scoping, which could be the `if` itself. To avoid scope confusion it's best to avoid doing this in the first place. I could be confusing JavaScript variable scope rules with Ruby's particular quirks, but the same principle is at work here. – tadman Oct 06 '17 at 03:17
  • Don't worry, I don't advocate doing this kind of thing at all exactly due to the confusion that it causes. The example in the question is only for illustrative purposes :) The potential of this issue being due to scoping within the `if` statement itself is interesting, so I might look into that. – Paul Fioravanti Oct 06 '17 at 03:28
  • This is not an answer to the question. You've written what to do to avoid the situation, but the mystery why `p name` and `puts "My name is #{name.inspect}"` give different results is still unsolved IMO. – Greg Oct 06 '17 at 05:14
  • @meta It's really confusing because of the *way* Ruby handles this, and there really should be a warning when you do it, but this is how it's behaved since at least 1.8.7. – tadman Oct 06 '17 at 07:01
  • 1
    I've added some more information here about how to discover what's going on internally using `defined?` and `local_variables`. – tadman Oct 06 '17 at 07:02
  • Nice, now it's very useful – Greg Oct 06 '17 at 07:19
  • 1
    Parentheses can also be used to avoid ambiguity, i.e. `name() || "Undefined"`. – Stefan Oct 06 '17 at 07:53
  • @tadman Thanks for the extra clarifying edits, but they seem to just confirm the expectations that I already outlined in the question. The issue I encountered while debugging was that `name` in the `if name.nil?` clause didn't actually seem to call out to the instance method as expected (otherwise it would have returned `"Paul"` like the previous `p name` statement did), but instead the reference seems to have switched over to the unassigned local variable. – Paul Fioravanti Oct 06 '17 at 08:27
0

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.

Paul Fioravanti
  • 16,423
  • 7
  • 71
  • 122