Love Your Self
Ruby’s Self
When you write ruby code, many times, the context in which your code is executed
matters. This context will contain an assumption as to the default Object
methods are being called on, self
. Within a class definition, this is
generally the class itself. Within an instance method definition, it is the
instance of that class, etc.
So,
example()
is functionally identical to
self.example()
The self
keyword is implied by the code context.
The implicit self is one of the things that lends ruby to expressive, programmer-friendly DSLs. The code looks nicer and easier to read. But it’s also part of what makes ruby (and rails) seem ‘magical’ sometimes. When you see
validates :user, presence: true
The intent is usually pretty clear, but it’s not always obvious what’s actually happening when the code is executed.
Using Self to Clarify Code
These DSL techniques can be powerful, but also somewhat obtuse.
My best example of this is the boilerplate ActionController code you see everywhere:
respond_to do |format|
format.html
format.xml { render xml: @record }
end
At first glance, you can guess what is happening here, but the more you consider the code, the less clear it is what is actually going on.
Stop and read that code for a bit, and decide what exactly is happening there.
It feels like something like this is happening:
if format == 'html' then output_default_formatting
elsif format == xml then output_xml_formatting
But that’s definitely not what’s written. There are no if
statements there; no
case
statement, no boolean logic at all… What is it actually doing?
self.respond_to() do |format|
format.html
format.xml { render xml: @record }
end
So, ActionController
implements a respond_to
method that takes a block and
supplies it a format
object. The format
object has methods for various
output types and each of those methods takes an optional block. We can surmise
that that block is only executed if the method and the requested output type
match.
Why is it implemented this way? I have absolutely no idea. A question for another day…
Anyway, if there is always an implicit self
target, why is there a self
keyword, and why do you have to use it sometimes?
Sometimes, it’s not just useful to think of the self
in order to understand
code, it’s necessary in order for the ruby parser to disambiguate software
intent.
When to use self
The self
keyword is required to remove ambiguity in some expressions:
Operator Overloading
When using methods like value=
or class []
, +
methods, etc. The ruby
default syntax takes precedent, so you need to explicitly state you are trying
to use the local one. Creating a temporary variable would take precedent over
the value=
method.
e.g.
class Dog
def name= (name)
@name = name
end
def something
name = "potato"
end
The name=
in the something
method would not be calling the name=
method
defined above. It would be creating a temporary variable called name. Then
assigning it the value ‘potato’, exiting the method, at which point that
variable would go out of scope. The same thing would happen using the implicit
setter method created by attr_accessor
.
Class Methods
Of course, we also use self to define class methods. You can do this two ways:
class TestClass
def self.message
@@message
end
end
You can also find this written as:
class TestClass
class << self
def message
@@message
end
end
end
Method Chaining
Users.where(subscribed: true).order(date_of_birth: :asc).limit(1)
Each method above returns an ActiveRecord::Relation
. The method alters
the scope of the query each time, and, since the return type is also
ActiveRecord::Relation
, these methods can be chained together for brevity and
clarity.
For method chaining, it doesn’t matter whether the same, mutated object is returned, or a new one of the same type. Here’s a simple counter:
class Counter
def initialize
@count = 0
end
def inc
@count +=1
end
def dec
@count -=1
end
def to_s
@count.to_s
end
end
c = Counter.new
c.inc
c.dec
c.inc
c.inc
c.inc
puts c
# => "3"
You can modify this slightly, so that this will still work, but you can also chain methods together:
class Counter
def initialize
@count = 0
end
def inc
@count +=1
self
end
def dec
@count -=1
self
end
def to_s
@count.to_s
end
end
c = Counter.new
c.inc
c.dec
c.inc
c.inc
c.inc
puts c
# => "3"
x = Counter.new
x.inc.dec.inc.inc.inc.inc.dec
puts x
# => "3"