Search code examples
rubyattr-accessor

How does method_name= work under the hood?


I was hacking a simple implementation of linked lists in Ruby and I came across something very peculiar.

I created a List class with an attr_accessor to a first_node. Something like this:

class List
  attr_accessor :first_node

  # Some code here ...
end

Then, while implementing the method delete_at I had the following error

linearly.rb:39:in `delete_at': undefined method `next' for nil:NilClass (NoMethodError) from linearly.rb:81:in `<main>'

This is a piece of the method:

def delete_at(position)
  if position == 0
    deleted_node = first_node
    first_node = first_node.next   # This is line 39.

    return deleted_node
  else
    # More code here ...

I forgot to use the class variable @first_node and instead I used the reader method first_node. Then, I started wondering why first_node returns nil when using first_node= in the same line.

Does it set @first_node to nil before setting a new value?

Notice that this piece of code works just fine:

def delete_at(position)
  if position == 0
    deleted_node = first_node
    first_node = deleted_node.next

    return deleted_node
  else
    # Some code here ...

EDIT:

This is how I call the implementation:

list = List.new
list.first_node = Node.new(1)
list.first_node.next = Node.new(2)
list.first_node.next.next = Node.new(3)

puts "Delete at 0"
puts list.delete_at(0)

Solution

  • This:

    first_node = first_node.next
    

    creates a local variable called first_node. Variable declarations are hoisted to the top of the scope in Ruby so your method is equivalent to:

    def delete_at(position)
      deleted_node = nil # Declare deleted_node and first_node
      first_node = nil   # as local variables.
      if position == 0
        deleted_node = first_node
        first_node = deleted_node.next
    
        return deleted_node
      else
        # Some code here ...
    

    That means that all first_node references in your method will be the local first_node variable rather than the getter and setting methods that attr_accessor creates for you.

    Either don't use variables that match method names or be explicit about what you mean by supplying the self receiver for the method calls:

    def delete_at(position)
      if position == 0
        deleted_node = self.first_node
        self.first_node = deleted_node.next
        #...