attr_accessor and NoMethodError

What am I missing here?

I have the following class

class Foo
  attr_accessor :x

  def initialize(x)
    @x = x
  end

  def count_down
    x -= 1
  end
end

If I then do

f = Foo.new(3)
f.count_down

I thought that I would decrement the x and then return 2, however I
get NoMethodError: undefined method '-' for nil:NilClass instead.

I thought that the attr_accessor just gave me a x and x= method,
which read and wrote to @x.

Even if I switch the count_down definition to:

def count_count
  x = x - 1
end

it fails with the same NoMethodError.


The implementation works, if you substitute the first x in count_down with either self.x or @x. However, as I stated above, I thought that attr_accessor gave me the following methods:

def x
  @x
end

def x=(other)
  @x = other
end 

and the line in count_down should in my opinion call x= with the x-1, where the x is calling the x method, retrieving @x and subtracting 1.

Shouldn’t Ruby look for all possible methods and variables, before declaring that the x = means “assign a new variable”?

1 Like

… the funny part is also the error message, which states that NoMethodError: undefined method '-' for nil:NilClass, which seems to indicate that it is the second x, which is read as nil, instead of the err being with the assigning part.

Here’s something interesting.

class Foo
  def initialize(x)
    @x = x
  end
  def inspect_x
    puts defined?(x)
    puts defined?(self.x)
    puts defined?(@x)
  end
  def x
    puts :reader
    @x
  end
  def x=(y)
    puts :writer
    @x = y
  end
  def cdx
    x -= 1
  end
  def cdselfx
    self.x -= 1
  end
  def cdatx
    @x -= 1
  end
end

x = Foo.new(3)
# => #<Foo:0x00000001381b60 @x=3> 

x.inspect_x
#method
#method
#instance-variable

x.cdx
#NoMethodError: undefined method `-' for nil:NilClass

x.cdselfx
#reader
#writer
# => 2 
x.cdselfx
#reader
#writer
# => 1 

x = Foo.new(3)
# => #<Foo:0x000000012fc438 @x=3> 
x.cdatx
# => 2 
x.cdatx
# => 1 

For some reason since you’re using assignment on an instance variable the instance reference is required regardless of whether you use the method or instance variable.

1 Like

If you add this method you can see the writer method isn’t getting called. x becomes a local (scope) variable, rather than the instance variable or method, when assigned.

class Foo
  def count_down
    y = x
    x = y-1
  end
end

x = Foo.new(3)

x.count_down
#reader
# => 2 
x.count_down
#reader
# => 2 

With some inspection

class Foo
  def count_down
    y = x
    x = y-1
    puts defined?(x)
    x
  end
end

x = Foo.new(3)

x.count_down
#reader
#local-variable
# => 2 
x.count_down
#reader
#local-variable
# => 2 

So to answer your question the -= operand takes x (local variable nil) to perform itself on. I just learned this myself. I don’t use attr methods… maybe ever.

1 Like

Yeah, so my question is:

When I type x = Ruby should probably check if there is a self.x= method, before assuming x is a new local variable.

1 Like

Do you think that might encourage (perhaps accidentally) inconsistent code? I think I prefer it as it is tbh…

I think you’re right. I just got caught in this simple trap writing some code yesterday. If you have a large class with many attr_reader, attr_writer and attr_accessor, it might get overwhelming to check all the instance variables before creating local variables.

2 Likes

This is an oldie but a goodie. Since variable assignment also creates a variable instance, you just have to be explicit with the scope (self).

This has led many ruby dsl authors don’t write setters, and instead dual purpose the getter with an optional argument.

1 Like

I can see why. :smirk:

class Foo
  def initialize(x)
    @x = x
  end

  def x(*args)
    return @x if args.empty?
    @x = args.first
  end
  
  def count_down
    x(x - 1)
   end
end

Now my count_down works. (Granted, in this simple case self.x or @x might have been better.)

1 Like

Normal behavor into Ruby world.

Accessor are here for outside access. Use self.x in this case (self access your object from outside, so you get access only to public methods). Without accessor, use @x to access your Instance Variable. It’s the normal way in Ruby. You define a @variable, you use @variable in your code.

In your case (x -= 1), internally, Ruby first get the x object (here x don’t exist, so it’s initialized to nil), and attempt to call on x the method “-” with your parameter (here 1). The fact that Ruby decompose this simple line in two actions can be checked here :

In irb, type :

x # You get undefined local variable or method x
x += 1 # You get the undefined method + for nil class
x # You get nil. Ruby in it's first action has created your x variable to nil

Great, no ?

This is why you get this strange error about a missing method : Numeric objects have the “-” method (and lot of other, but it’s not our problem here). Nill object have not. So you get a method error. Really, it’s normal.

In irb, try to do :

1.respond_to? :-

Prefer use @x in your code and let the use of your accessors for outside access only.
Always use self when you want use setter following assignment signature, in short : Any method name ending with “=”… And especially if you use object with dynamic method generation like ActiveRecord does (at least, one method ending with “=” for each field).

I hope this help you.
Ludovic.

1 Like

Prefer this code :

class Foo
  attr_accessor :x
  def initialize(x=0)
    @x = x if x.is_a? Numeric
    @x ||= 0
  end

  def count_down
    @x -= 1
  end
end
1 Like

Same kind of thing in the global namespace.

public def x
  puts :reader
  $x
end

public def x=(other)
  puts :writer
  $x = other
end

$x = 3

x -= 1
#NoMethodError: undefined method `-' for nil:NilClass
x
# => nil
self.x
#reader
# => 3
self.x -= 1
#reader
#writer
# => 2 
self.x
#reader
# => 2
2 Likes

And then there’s send

class Foo
  attr_accessor :x

  def initialize(x)
    @x = x
  end

  def count_down
    send :x=, x()-1
  end
end
a = Foo.new(3)
puts a.count_down
# => 2
puts a.count_down
# => 1