CodeGnome CodeGnome - 1 month ago 4
Ruby Question

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.

Answer

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[.]