Cover Image credit: Samier Saeed and SitePoint.
Have you ever wondered what a "singleton class" is? Have you ever been talking to someone or reading a blog post and "singleton class" or "singleton method" got used, and you just smile and nod, making a mental note to look it up later? Now is your time. Now is your moment. I'm hoping to explain this concept in more intuitive language and show you how handy it can be.
Side note: a lot of this information came from reading The Well-Grounded Rubyist by David A. Black. This book has a ton of great information and is currently one of my favorite books on Ruby.
Side note #2: At the very least, scroll down to the bottom for some cheesy and wholesome singleton-based positivity. 😁
The Code
If you've written much Ruby, you've used these "singleton classes" already without knowing it! First, I'll show you the code that you've probably already written, so you have some context.
class Config
def self.from_file(filename)
Config.new(YAML.load_file(filename))
end
end
dev_config = Config.from_file("config.dev.yaml")
# => Config object with dev settings
You may have also seen it like this:
module Geometry
class << self
def rect_area(length, width)
length * width
end
end
end
Geometry.rect_area(4, 5)
# => 20
Up until now, you've probably referred to these as "class methods." You are mostly right. But why do they work? What's happening here?
Individualization
This is a concept that is central to what makes Ruby so awesome. Individual objects, even of the same class, are different from each other, and they can have different methods defined on them. I'm going to shamelessly use our pets to aid in this example.
class Pet
def smolder
"Generic cute pet smolder"
end
end
succulent = Pet.new
momo = Pet.new
willy = Pet.new
def momo.smolder
"sassy cat smolder"
end
def willy.smolder
"well-meaning dingus smolder"
end
Now, when we call smolder
on succulent
, which we haven't changed, things go as planned.
succulent.smolder
# => Generic cute pet smolder"
But when we call smolder
on willy
or momo
, something different happens!
momo.smolder
# => "sassy cat smolder"
willy.smolder
# => "well-meaning dingus smolder"
So, how does this work? Did we re-define smolder
for each pet? Do me a favor and check out the output of the following:
succulent.singleton_methods
# => []
momo.singleton_methods
# => [:smolder]
willy.singleton_methods
# => [:smolder]
That's right! You're using a singleton method! Now, I think, we're ready to talk about what a singleton class is.
What is a Singleton Class?
First, a more general programming, less Ruby-specific question: what is a singleton? While there are various definitions that might be more specific for different cases, at its core, a singleton is just something that there is only one of. It is the only one of its kind.
What does that mean in the context of Ruby? Here it is: when you instantiate an object from a class in Ruby, it knows about the methods that its class gives it. It also knows how to look up all of the ancestors to its class. That's why inheritance works.
"Oh, my class doesn't have that method? Let's check its parent class. And that class's parent class. Etc."
One of the cool things about Ruby is that the ancestry chain is very unambiguous by design. There is a specific set of rules by which objects search their ancestors, such that there is never any doubt which method gets called.
In addition to knowing about its class, each object is created with a singleton class that it knows about. All the singleton class is is a kind of "ghost class" or, more simply, a bag to hold any methods that are defined only for this particular object. Try this out:
momo.singleton_class
# => #<Class:#<Pet:0x00007fea40060220>>
In the inheritance hierarchy, it sits invisibly, just before the objects actual class. However, you can't see it by looking at the object's ancestors.
momo.class.ancestors
# => [Pet, Object, Kernel, BasicObject]
But if we look at the ancestry tree for the singleton class itself:
momo.singleton_class.ancestors
# => [#<Class:#<Pet:0x00007fea40060220>>, Pet, Object, Kernel, BasicObject]
You can see that it comes in right at the beginning. Thus, when momo
goes to look for the smolder
method, it looks first in its singleton class. Since there is a smolder
method there, it calls that one, instead of looking further up the tree to the one defined in the Pet
class.
What Does This Have to Do with Class Methods?
Now is when we start to see the power of the singleton class. Don't forget that every class is just an object of the class Class
. If that sentence made you start to hyperventilate, don't worry. I'll explain.
Pet.class
# => Class
And Class
is just a class that provides some methods to every instance of it (classes) you create, just like any other class.
Class.instance_methods(false)
# => [:new, :allocate, :superclass]
So, really, when you're defining "class methods" that you plan to call directly on the class, what you're actually doing is defining methods on that particular Class object — in its singleton class!
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
Pet.singleton_methods
# => [:random]
And… if the singleton class exists, it becomes the parent class to singleton_classes of classes that inherit from the main class. An example should help.
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
class Reptile < Pet
def self.types
%w{lizard snake other}
end
end
Reptile.singleton_methods
# => [:types, :random]
Reptile.singleton_class.ancestors
# => [#<Class:Reptile>, #<Class:Pet>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
See how Reptile
's singleton class inherits from Pet
's singleton class, and thus the "class methods" available to Pet
are also available to Reptile
?
Other Tidbits
I think so far, we've pretty much covered all of the important bits. There are, however, a couple more interesting details that I thought were cool that are sort of tangentially related: the somewhat hard to decipher class << self
syntax, the different ways of creating class methods, and the use of extend
. Feel free to read on if you're interested.
Class << self
There are actually two ways to use the class
keyword: directly followed by a constant (a la class Gelato
), or followed by the "shovel operator" and an object (a la class << momo
). You already know about the first one — it's the way you usually declare a class! Let's focus on the second one, which is the syntax to directly open up an object's singleton class. You can think about it as essentially the same as defining methods like we were doing above.
What I mean is this:
# This:
def momo.snug
"*snug*"
end
# is the same (pretty much) as this:
class << momo
def snug
"*snug*"
end
end
You do this all the time when you re-open regular classes to add more functionality.
class Gelato
attr_reader :solidity
def initialize
@solidity = 100
end
def melt
@solidity -= 10
end
end
# And re-open it to add one more method
class Gelato
def refreeze
@solidity = 100
end
end
dessert = Gelato.new
5.times { dessert.melt }
dessert.solidity
# => 50
dessert.refreeze
# => 100
The syntax class << object; end
is just another way of re-opening the object's singleton class. The benefit here is that you can define constants and multiple methods all at once instead of one at a time.
# Instead of:
def momo.pounce
"pounce!"
end
def momo.hiss
"HISS"
end
def momo.lives
9
end
# We can do
class << momo
def pounce
"pounce!"
end
def hiss
"HISS"
end
def lives
9
end
end
momo.singleton_methods
# => [:pounce, :hiss, :lives, :smolder]
It's a common pattern when adding multiple class methods to a class to see the following:
class Pet
class << self
def random
%w{cat dog bird fish banana}.sample
end
end
end
# Which, since "self" is inside of the class
# declaration, means that 'self == Pet', so you could
# also do this:
class Pet
class << Pet
def random
# ...
end
end
end
Maybe you've seen this pattern and not known what it is, or maybe you knew it adds class methods but didn't know what why. Now you know! It's all thanks to the singleton class!
class << self
vs def self.method
vs def Pet.method
There are a few different ways to declare class methods.
# 1. In global scope
def Pet.random
%w{cat dog bird fish banana}.sample
end
# 2. Inside the class definition, using 'self'
class Pet
def self.random
%w{cat dog bird fish banana}.sample
end
end
# 3. Inside the class definition, using the shovel
class Pet
class << self
def random
%w{cat dog bird fish banana}.sample
end
end
end
# 4. Outside the class definition, using the shovel
class << Pet
def random
%w{cat dog bird fish banana}.sample
end
end
So what's the difference?? When do you use one or the other?
The good news is that they're all basically the same. You can use whichever one makes you the happiest and matches the style of your codebase. The only difference is with #3, and how it deals with constants and scope.
MAX_PETS = 3
def Pet.outer_max_pets
MAX_PETS
end
class Pet
MAX_PETS = 1000
class << self
def inner_max_pets
MAX_PETS
end
end
end
Pet.outer_max_pets
# => 3
Pet.inner_max_pets
# => 1000
See that the inner_max_pets
function has access to the scope inside the Pet
class and the constants there? That's really the only difference. Feel free to carry on using your favorite syntax with confidence.
Using Extend to Safely Modify Built-In Classes
Hopefully, at some point, you've read a blog post or had someone warn you about the dangers of re-opening built-in Ruby classes. Doing something like the following should be done veeeery carefully.
class String
def verbify
self + "ify"
end
end
"banana".verbify
# => "bananaify"
The dangers include accidentally overwriting built-in methods, having methods clash with other libraries in the same project, and generally making things not behave as expected. The extend
keyword can help with all of that!
What is Extend?
The extend
keyword is a lot like include
in that it allows you to load functionality into your class/module from other classes/modules. The difference, however, is that extend
puts these methods onto the target object's singleton class.
module Wigglable
def wiggle
"*shimmy*"
end
end
willy.extend(Wiggleable)
willy.singleton_methods
# => [:wiggle, :smolder]
Thus, if you use extend
inside a class definition instead of include
, the methods will get added to the class's singleton class as class methods instead of being added to the class itself as instance methods.
module Hissy
def hiss
"HISS"
end
end
class Reptile
extend Hissy
end
snek = Reptile.new
snek.hiss
# => Error! Undefined method hiss for 'snek'
Reptile.hiss
# => "HISS"
How Does That Help Us?
So, let's say that we really needed to have that verbify
method on the strings we were using. While you could create and use a subclass of String
, another option would be to extend individual strings!
module Verby
def verbify
self + "ify"
end
end
noun = "pup"
noun.extend(Verby)
noun.verbify
# => "pupify"
Cheesy Wrap Up
So remember, singletons aren't just an intimidating-sounding but not-super-complicated Ruby topic. You are the real singleton — yes, you're a human, but there's nobody else quite like you. You've got class and your own methods of doing things, and that's valuable. And now we've just added a little more functionality to you.
class << you
def use_singletons_for_fun_and_profit
# ...
end
end