Ruby Selfie
2022-04-09
I don't do much ruby these days, but I always loved the object model and nature of self
in the language.
The more I learned about it, the more cool and fun things I could do.
And then there's that special feeling when the infamous extend self
and class << self
stopped feeling like magic incantations,
and just made sense in the context of ruby's object model.
It started with the understanding that everything in ruby is an object which is an instance of a class.
Even classes themselves are objects that are instances of the class Class
.
class Dog
end
puts Dog.class # => Class
When you use the class keyword, you are "opening" a class to define instance methods for any instance of the class:
class Dog
def bark
puts "bark!"
end
end
dog1 = Dog.new
dog2 = Dog.new
dog1.bark # => "bark!"
dog2.bark # => "bark!"
But class instance methods are really class methods on the class's singleton class.
Every object in ruby has an "invisible" singleton class (a metaclass):
puts Dog.singleton_class # => #<Class:Dog>
puts Dog.new.singleton_class # => #<Class:#<Dog:0x0123...>>
puts Dog.new.singleton_class # => #<Class:#<Dog:0x0456...>>
When you say def <some object>.<some method>
, you are defining an instance method on that object's singleton class:
def dog1.bark
puts "redefined bark!"
end
When you call a method in ruby, it first checks the object's singleton class:
dog1.bark # => redefined bark!
dog2.bark # => bark!
Since classes are objects, the same applies:
def Dog.eat
puts "yum!"
end
We defined an instance method on the class object's singleton class (wee), and method lookup works the same way. There's nothing special about "class" methods in ruby!
Dog.eat # => "yum!"
Also, there's always an implicit self
object:
puts self # => main
and when you open a class, self changes to the class object:
class Dog
puts self # => Dog
end
So when you define "class methods" in ruby like this:
class Dog
def self.eat_again
puts "yummy!"
end
end
Dog.eat_again # => yummy!
You're using the same def <some object>.<some method>
pattern, which defines instance methods on the object's singleton class, and here object is the class object.
Radical!
Another way to define methods in ruby is with modules:
module Pisser
def piss
puts "pisss"
end
end
Classes have a method called include
which takes a module and adds the
module methods to the class as instance methods:
Dog.include(Pisser)
dog1.piss # => pisss
dog2.piss # => pisss
Classes also have a method called extend
which is described as adding "class
methods" to the class, but to understand extend self
, you need to understand
that really extend
is adding instance methods to the class' singleton
class. Class methods are a ruby myth!
Dog.extend(Pisser)
Dog.piss # => pisss
Also remember that using class Whatever
is just opening the Whatever class,
and self
becomes the class, which means this is the same as calling
Dog.extend(Pisser)
:
class Dog
extend Pisser # => here the implicit `self` is `Dog`
end
The same thing applies to modules....
module AnotherPisser
extend Pisser
end
AnotherPisser.piss # => pisss
Which means we can define modules to namespace "functions" (that are really just instance methods on the module's singleton class):
module Util
extend self # => the same as Util.extend(Util)
# Since Util extends itself, these methods will exist on Util's singleton
# class, making them callable directly on Util
def hello
puts "hello"
end
end
Util.hello # => hello
Another way to define methods on an object's singleton class is with the
class << <object>
syntax. For example:
class << dog1
def pet
puts "tail wag!"
end
end
dog1.pet # => tail wag!
Which means...
class Dog
class << self
def zen
puts "ruby is radical!"
end
end
end
Inside the Dog class, self
is the Dog class object, so class << self
opens
the Dog class object's singleton class to define methods, and ruby checks the
singleton class first on method lookups:
Dog.zen # => ruby is radical!
The easy way to think about it is extend self
is a convenient way to define
"module methods" and class << self
is a convenient way to define "class
methods", but understanding the object model and method lookup shows there
is nothing special going on. It's all just instance methods on objects.