Search code examples
rubysetterinstance-variablesread-eval-print-looptoplevel

Why does a Ruby toplevel assignment method fail to assign instance variables in the REPL?


Setter Works Inside a Class; Fails in REPL Top-Level

In a related question, I was trying to understand why an assignment method was returning an unexpected value, and learned that this is a surprising but documented edge case in Ruby. However, when I was attempting to debug the problem, I went further down the rabbit hole and ran into some additional surprises that I can't explain.

Setter Inside a Class

When I have a setter method inside a class such as:

class Setter
  def foo=(bar)
    @foo = Integer(bar).succ
  end
end

then I get the documented oddness with return values from the setter method, but the instance variable is still set correctly. For example:

s = Setter.new
s.foo = 1
#=> 1

s.instance_variable_get :@foo
#=> 2

Setter in REPL Top-Level Object

However, at the REPL (e.g. Pry or IRB), the instance variable is never actually set, even though my understanding is that instance variables ought to be stored in the toplevel "main" object:

self.name
#=> NoMethodError: undefined method `name' for main:Object

# This is expected to set the @foo instance variable for main.
def foo= int
  @foo = int
end

foo = 1

@foo
#=> nil

instance_variable_get :@foo
#=> nil

TOPLEVEL_BINDING.eval('self').instance_variables
#=> []

And yet, the toplevel object does store instance variables! For example:

@bar = 1 + 1; @bar
#=> 2

instance_variable_get :@bar
#=> 2

The Question, Restated

Given that the REPL stores instance variables, why does the class assignment method work while the toplevel assignment method fails? I would expect both to function the same way.


Solution

  • Ruby's assignment operator = will create a local variable if you don't explicitly write out the receiver. In your case:

    foo = 1
    

    is creating a local variable foo rather than calling the method foo=. You'll have to use

    self.foo = 1
    

    To actually call the method you defined above. Now that will set @foo:

    def foo= i # define foo= on self
      @foo = i
    end
    #=> :foo=
    
    foo = 3
    #=> 3
    
    @foo
    #=> nil
    
    foo # here's the new local variable
    #=> 3
    
    instance_variables
    #=> [:@prompt]
    
    instance_variable_get :@foo 
    #=> nil
    
    self.foo = 4 # now calling the foo= method
    #=> 4
    
    foo # local foo is still 3
    #=> 3
    
    @foo # now the ivar is set
    #=> 4
    

    In your class example, you have an explicit receiver with s.foo = 1. Ruby then knows you're calling the foo= setter on s. The assignment methods documentation says:

    When using method assignment you must always have a receiver. If you do not have a receiver, Ruby assumes you are assigning to a local variable[.]