LaunchKit
All quizzes
Ruby 1 views

An assignment behind if false still shadows your method

Question

What is the return value of the following Ruby code?

class Person
  def firstname = "Jon"
  def lastname = "Jones"

  def fullname
    lastname = "Snow" if false

    "#{firstname} #{lastname}"
  end
end

Person.new.fullname # => ???

The correct answer is

Explanation

TL;DR

The parser registers lastname as a local variable the moment it reads the assignment lastname = "Snow", even though the if false guard means the assignment never executes. From that line onward, the bare word lastname refers to that local variable, which holds its default value of nil, not to the Person#lastname method. firstname is still a method call, so the interpolation produces "Jon ".

Disasm

To see the mechanism, reference lastname both before and after the assignment, then disassemble the method:

class Person
  def lastname = "Jones"

  def fullname
    lastname
    lastname = "Snow" if false
    lastname
  end
end

puts RubyVM::InstructionSequence.disasm(Person.new.method(:fullname))

Output (the == disasm header line is trimmed):

local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] lastname@0
0000 putself                                                          (   5)[LiCa]
0001 opt_send_without_block                 <calldata!mid:lastname, argc:0, FCALL|VCALL|ARGS_SIMPLE>
0003 pop
0004 getlocal_WC_0                          lastname@0                (   7)[Li]
0006 leave                                                            (   8)[Re]

The local table already contains a lastname@0 slot before a single instruction runs. The slot exists because the parser saw an assignment, not because one executed.

The reference on line 5, before the assignment, compiles to a method call:

opt_send_without_block   <calldata!mid:lastname, argc:0, ...>

The reference on line 7, after the assignment, compiles to a local variable read:

getlocal_WC_0   lastname@0

Note that the if false branch itself was optimized away entirely, yet its assignment still changed how the later reference compiles.

The rule: local variables are created at parse time. Once an assignment to a name appears, every later bare reference to that name in the same scope reads the local variable, which defaults to nil when the assignment never ran. To reach the method anyway, force a method call with an explicit receiver or parentheses: self.lastname or lastname() both return "Jones".

Share this quiz

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.