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