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.
Ruby has three helpful functions you can use to do things like this:
attr_accessor :x
is equivalent to definingdef x
anddef x=
methodsattr_reader :x
is equivalent to defining just adef x
methodattr_writer :x
is equivalent to defining just adef 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