0

Given the code:

class Someone

  def full_name
    if false # on purpose
      # We'll never reach this point because of the `false` above
      first_name = "Other" # So how this code can affect 
                           # the instance variable?
    end
    "#{first_name} #{last_name}"
  end

  def first_name
    "First"
  end

  def last_name
    "Last"
  end
end

s = Someone.new

s.full_name
# => "Last"

Why s.full_name == "Last".

In other words, how first_name method can be overriden until we don't pass through the if statement ?…

To be clear, why ruby doesn't act as… javascript, for instance:

class Someone {

  get full_name() {
    if ( false ) {
      // In JS, that doesn't override
      // first_name
      this.first_name = "Other";
    }
    return this.first_name + ' ' + this.last_name;
  }

  get first_name() {
    return "First";
  }
  
  get last_name() {
    return "Last";
  }
}

let s = new Someone();

console.log("s.full_name() = ", s.full_name);

s.full_name will be egal to "First Last", not to "Last" as in ruby.

It's my effort to understand ruby, not to blame it! (I love passionnément Ruby)

Thanks a lot for answers.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • Very very very sorry for typo in question ("override"). –  Oct 22 '17 at 14:10
  • There was an answer here before (it's been deleted) that I thought had a decent explanation. The key is that the Ruby parser decides whether to treat it as a local variable before the interpreter ever sees whether it's run. – Max Oct 22 '17 at 15:35
  • Thanks, @Max. Perfectly clear, and it's what I suspected. Thanks for help. –  Oct 22 '17 at 15:41
  • @Max You may be interested by the quotation from The Pickaxe about the heuristic used by Ruby in my answer. – BernardK Oct 22 '17 at 17:04
  • @BernardK Thanks! I'd been looking for an authoritative reference, so I'm glad you found that. – Max Oct 22 '17 at 18:04
  • @JörgWMittag I have a great respect for your Ruby knowledge and SO contribution, but I disagree that this is a duplicate. The other post is about "why a variable defined in a falsy block exists ?", this one is about the heuristic that Ruby uses to choose between variable and method call. See also tadman 's answer. – BernardK Oct 23 '17 at 11:18

3 Answers3

1

We need to trace it to understand what's going on.

class Someone

  def full_name
    if false # on purpose
      # We'll never reach this point because of the `false` above
      first_name = "Other" # <--- first_name stored as variable
    end

    puts '---> about to call methods first_name and last_name ... or not'
    "#{first_name} #{last_name}" # <--- big decision here
    puts '... which method wal called ???'
  end

  def first_name
    puts 'passes in first_name'
    "First"
  end

  def last_name
    puts 'passes in last_name'
    "Last"
  end
end

s = Someone.new

puts s.full_name.inspect

Execution :

$ ruby -w t.rb 
t.rb:10: warning: possibly useless use of a literal in void context (because of the following puts)
---> about to call methods first_name and last_name ... or not
passes in last_name
... which method wal called ???
nil (value returned by the last puts in full_name)

So why the expression "#{first_name} #{last_name}" calls last_name but not first_name ? The answer is in The Pickaxe :

Variable/Method Ambiguity

When Ruby sees a name such as a in an expression, it needs to determine whether it is a local variable reference or a call to a method with no parameters. To decide which is the case, Ruby uses a heuristic. As Ruby parses a source file, it keeps track of symbols that have been assigned to. It assumes that these symbols are variables. When it subsequently comes across a symbol that could be a variable or a method call, it checks to see whether it has seen a prior assignment to that symbol. If so, it treats the symbol as a variable; otherwise, it treats it as a method call.

...

Note that the assignment does not have to be executed—Ruby just has to have seen it.

This is exactly the situation you've encountered. Rename first_name to something else :

  xxxfirst_name = "Other" # So how this code can affect 

and ... tada !

$ ruby -w t.rb 
t.rb:10: warning: possibly useless use of a literal in void context
t.rb:6: warning: assigned but unused variable - xxxfirst_name
---> about to call methods first_name and last_name ... or not
passes in first_name
passes in last_name
... which method wal called ???
nil

or, without the last puts :

$ ruby -w t.rb 
t.rb:6: warning: assigned but unused variable - xxfirst_name
---> about to call methods first_name and last_name ... or not
passes in first_name
passes in last_name
"First Last"
   first_name = "Other" # So how this code can affect 
                        # the instance variable?

Your code has no instance variable (beginning with an @). first_name defines a local variable which is discarded when the method ends.

Add inspect in the expression :

"#{first_name.inspect} #{last_name}"

The result is now :

passes in last_name
"nil Last"

As the if was not executed, the variable is nil.

BernardK
  • 3,674
  • 2
  • 15
  • 10
1

As mentioned by others, the behaviour happens during the parsing process.

To investigate, you can use Ripper.

require 'ripper'
require 'pp'

pp Ripper.sexp("a = 3")
# [:program,
#  [[:assign, [:var_field, [:@ident, "a", [1, 0]]], [:@int, "3", [1, 4]]]]]

pp Ripper.sexp("a = 3; a")
# [:program,
#  [[:assign, [:var_field, [:@ident, "a", [1, 0]]], [:@int, "3", [1, 4]]],
#   [:var_ref, [:@ident, "a", [1, 7]]]]]

pp Ripper.sexp("def a; end; a")
# [:program,
#  [[:def,
#    [:@ident, "a", [1, 4]],
#    [:params, nil, nil, nil, nil, nil, nil, nil],
#    [:bodystmt, [[:void_stmt]], nil, nil, nil]],
#   [:vcall, [:@ident, "a", [1, 12]]]]]

pp Ripper.sexp("if false; a = 3; end; def a; end; a")
# [:program,
#  [[:if,
#    [:var_ref, [:@kw, "false", [1, 3]]],
#    [[:assign, [:var_field, [:@ident, "a", [1, 10]]], [:@int, "3", [1, 14]]]],
#    nil],
#   [:def,
#    [:@ident, "a", [1, 26]],
#    [:params, nil, nil, nil, nil, nil, nil, nil],
#    [:bodystmt, [[:void_stmt]], nil, nil, nil]],
#   [:var_ref, [:@ident, "a", [1, 34]]]]]

You can play a bit with this great library and look for var_refs and vcalls.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
0

This is a confusing little bit of Ruby's variable vs. method call ambiguity, but it's actually fairly easy to resolve if you know what you're looking for. The easiest way to find out what something's being interpreted as is to ask:

def full_name
  p defined?(first_name)
end

In this case you get "method" since in that code there's no reason to believe otherwise.

Once you add a local variable, though, even one that never gets used, the interpretation changes, but it only changes after that line where the shift in usage occurs:

def first_name
    "First"
end

def full_name
  p defined?(first_name)

  if (false)
    first_name = :not_used
  end

  p defined?(first_name)
end

full_name

Now you get "method" for the first one, but "local-variable" for the second, as the interpretation has shifted. The very presence of first_name = has caused this twist. It cannot be undone in that block, the only thing you can do is avoid conflicting names.

You can also call local_variables to find out which names are potentially variables, as those are known in advance.

BernardK
  • 3,674
  • 2
  • 15
  • 10
tadman
  • 208,517
  • 23
  • 234
  • 262
  • Thanks for you explaination, @tadman! In fact, it's an issue I used to uncounter when I wrote (too huge) programs with no testing. Hopefully, it never happens again but this question of noob never left my mind. –  Oct 23 '17 at 06:18
  • I use my recently acquired editor privilege to make your code work as described :) Without `def first_name`, the first `p defined?(first_name)` returns `nil` instead of `"method"`. – BernardK Oct 23 '17 at 12:55