Skip to content

Instantly share code, notes, and snippets.

@noonat
Last active August 29, 2015 14:08
Show Gist options
  • Save noonat/25431f36c3009bd90d10 to your computer and use it in GitHub Desktop.
Save noonat/25431f36c3009bd90d10 to your computer and use it in GitHub Desktop.

Understanding Accessor Methods in Ruby

Accessing properties of an object in Ruby are a bit different than they are in JavaScript. In JavaScript, if you defined an object constructor like so:

function Foo() {
  this.x = 1;
  this.y = 2;
}

You could access the properties of an instance of Foo like so:

> foo = new Foo();
{ x: 1, y: 2 }
> foo.x
1
> foo.y
2

But if you define a similar class in Ruby:

class Foo
  def initialize
    @x = 1
    @y = 2
  end
end

Trying to access the properties has a very different result:

[1] pry(main)> foo = Foo.new
=> #<Foo:0x007fb421911888 @x=1, @y=2>
[2] pry(main)> foo.x
NoMethodError: undefined method 'x' for #<Foo:0x007fb421911888 @x=1, @y=2>
from (pry):8:in '__pry__'
[3] pry(main)> foo.y
NoMethodError: undefined method 'y' for #<Foo:0x007fb421911888 @x=1, @y=2>
from (pry):9:in '__pry__'

What happened here? Well, the meaning of @x = 1 in Ruby, and the meaning of this.x = 1 in JavaScript are basically the same. But the meaning of foo.x in JavaScript and Ruby is very different. When you type foo.x in Ruby, this is equivalent to foo.x() -- Ruby is trying to call a method named x on your instance, not access the x property.

You can think of properties (or "instance variables") in Ruby as private. They are something you create that your class uses internally. You need to create methods if you want other things to access them.

In JavaScript, that would mean doing something more like this:

function Foo() {
  this._x = 1;
  this._y = 2;
}

Foo.prototype.x = function() {
  return this._x;
};

Foo.prototype.y = function() {
  return this._y;
};

Which ends up doing the same thing as before:

> foo = new Foo()
{ _x: 1, _y: 2 }
> foo.x()
1
> foo.y()
2

In Ruby, you could do that like this:

class Foo
  def initialize
    @x = 1
    @y = 2
  end

  def x
    @x
  end

  def y
    @y
  end
end

And now things work as expected:

[4] pry(main)> foo = Foo.new
=> #<Foo:0x007f8f44b99ee0 @x=1, @y=2>
[5] pry(main)> foo.x
=> 1
[6] pry(main)> foo.y
=> 2

But what if you wanted to set the value in Ruby?

[7] pry(main)> foo.x = 3
NoMethodError: undefined method 'x=' for #<Foo:0x007f8f44b99ee0 @x=1, @y=2>
from (pry):18:in '__pry__'

Well, that's still broken. But the error we see here is a bit of a hint: it says that the method x= is undefined. That's because whenever you set an attribute of an object in Ruby, such as foo.x = 1, it's actually trying to evaluate foo.x=(1), where x= is a named method on the object! That means you can just redefine Foo like so:

class Foo
  def initialize
    @x = 1
    @y = 2
  end

  def x
    @x
  end

  def x=(value)
    @x = value
  end

  def y
    @y
  end
end

And now you can set the value of x:

[8] pry(main)> foo = Foo.new
=> #<Foo:0x007f8f469750e0 @x=1, @y=2>
[9] pry(main)> foo.x = 3
=> 3

But note that y still gives an error, because we didn't define a setter method for that:

[10] pry(main)> foo.y = 4
NoMethodError: undefined method 'y=' for #<Foo:0x007f8f469750e0 @x=3, @y=2>
from (pry):40:in '__pry__'

This is useful if you want to make an attribute read-only.

This is all pretty verbose, though. This Ruby object is much longer than the original JavaScript object. Luckily, Ruby provides shortcuts that allow us to specify this behavior in a much simpler way.

Using the Helpers

Ruby has three helpful functions you can use to do things like this:

  • attr_accessor :x is equivalent to defining def x and def x= methods
  • attr_reader :x is equivalent to defining just a def x method
  • attr_writer :x is equivalent to defining just a def x= method

For example, if you had this class instead:

class Foo
  attr_accessor :a, :b
  attr_reader :c
  attr_writer :d

  def initialize
    @a = 1
    @b = 2
    @c = 3
    @d = 4
  end
end

You can read and write a and b:

[11] pry(main)> foo = Foo.new
=> #<Foo:0x007f8f46a25210 @a=1, @b=2, @c=3, @d=4>
[12] pry(main)> foo.a
=> 1
[13] pry(main)> foo.a = 10
=> 10
[14] pry(main)> foo.b
=> 2
[15] pry(main)> foo.b = 20
=> 20

You can only read c:

[18] pry(main)> foo.c
=> 3
[19] pry(main)> foo.c = 30
NoMethodError: undefined method 'c=' for #<Foo:0x007f8f46a25210 @a=100, @b=200, @c=3, @d=4>
from (pry):71:in '__pry__'

And you can only set d:

[20] pry(main)> foo.d
NoMethodError: undefined method 'd' for #<Foo:0x007f8f46a25210 @a=100, @b=200, @c=3, @d=4>
from (pry):72:in '__pry__'
[21] pry(main)> foo.d = 40
=> 40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment