Ruby Bareword Assignment and Method Calls With Implicit Self

2012/1/16

Problem

If I do this:

puts foo
foo = 3

there is always the doubt whether I'm accessing a local variable, or calling methods foo and foo=.

TL;DR

When you want to call an instance's own methods, use self:

self.foo             # Calls foo
self.foo = 'bar'     # Calls foo=

Example 1

def example1
 'example1 method'
end
example1 #=> "example1 method"
example1 = 'assigned value'
example1 #=> "assigned value"

Here, we define a method, and then make an assignment. As we assign to a bareword, Ruby creates a new local variable.

As soon as a value is assigned to the local variable, the method no longer gets called.

Example 2

But, what if we also have an assignment method?

def example2
 'example2 method'
end

def example2=(value)
 puts "example2= called" # (this never gets called)
end

example2 #=> "example2 method"
example2 = 'assigned value'
example2 #=> "assigned value"

Adding the method example2= does not change things. When we assign to a bareword, Ruby takes it as assignment to a local variable.

Example with a Class

class Foo
 attr_accessor :bar

 def initialize
   @bar = 42
 end

 def method1
   puts bar
 end

 def method2
   bar = 99
   puts bar
 end

 def method3
   bar = 99
   puts self.bar
 end
end

foo = Foo.new
foo.bar  #=> 42
foo.method1 #=> 42
foo.method2 #=> 99
foo.method3 #=> 42

method2 is the problem case. bar is assigned to, creating a local variable, so subsequent calls to bar return 99.

method3 disambiguates by explicitly calling the bar method on self.

The Cause

There are two things going on here:

  1. bareword assignment creates local variables,

  2. local variables mask methods of the same name.

Refactoring Might Break Code

One solution is to use `self.method` only in cases where local variables mask methods. The problem with this approach is that code may be altered, introducing local variables, and so altering the behaviour of following code:

Original Code

class Foo
 attr_accessor :bar

 def baz
   puts bar
 end
end

foo = Foo.new
foo.bar = 42
foo.baz #=> 42

Modified Code

class Foo
  attr_accessor :bar

  def baz
    bar = 99 # <= variable assignment introduced
    puts bar
  end
end

foo = Foo.new
foo.bar = 42
foo.baz #=> 99

Solution

The best solution is to always call instance methods on self.