Getting the Singleton Class of a BasicObject in Ruby
Ruby objects provide the method singleton_class
which returns the
object's singleton class. Unfortunately, BasicObject
doesn't have
this because it's Object
's superclass. So to get it, we need to be
somewhat clever.
And having spent way too much time figuring out how to do this, I'm writing it here so a) that I don't lose it again and b) so that others will have less trouble than me. (I'm not on Medium so, um, hello from the fourth page of your Google search results.)
TL; DR, How do I do it?
In an instance, you'd do something like this:
obj = BasicObject.new
obj.instance_exec(obj) {
class << self
lself = self
self.define_method(:my_singleton_class) { lself }
end
}
Notice how I copy self
to lself
on line 4. That's because self
will have changed when the method is called but the block that forms
the body of my_singleton_class
captures the local variable.
Also: this won't work on Ruby versions from sometime before 2.7
because define_method
is private before then; see your version's
Module
documentation for define_method
for a hacky workaround if
it's too old.
Doing this with a BasicObject
subclass is even simpler:
class Thingy < BasicObject
def my_singleton_class
class << self
return self
end
end
end
What's it good for?
Any case where you want an object to handle a method call by doing something other than call a method. For example, a DSL or a proxy object that forwards the call to something else.
Typically, you'd create a class with no methods, then implement
method_missing
to catch the failing method lookups and do the right
thing with them.
class Proxy
def initialize(target) @target = target; end
def method_missing(name, args)
log "Called #{name} with #{args}"
return @target.send(name, args)
end
end
BasicObject
is the ideal base class for this because it has very few
methods but if that's not enough–if you need to get rid of those few
as well–you can always override (most of) them with a method that
calls method_missing
directly. This is straightforward when
creating a subclass but there are times when it's necessary or easier
to add methods to the object instead, and for that you need to get the
singleton class.
In my case, I'm writing a DSL where every method whose name starts
with a letter is valid; this means they all need to turn into calls to
method_missing
.
(Handling the case where the user uses method_missing
as a name in
the DSL is left as an exercise to the reader.)
What's a singleton class anyway?
So normally in OOP, an object is an instance of a class and this is the case with Ruby as well:
[] # => []
[].class # => Array
[].class.class # => Class
But, when Ruby creates an object from a class, it also first creates
another anonymous class called the singleton class
. This gets
inserted in the new object's inheritance heirarchy: that is, the
singleton class becomes a subclass of the new object's class and the
object becomes an instance of the singleton instead of the original
class.
x = [] # => []
x.class # => Array
x.singleton_class # => #<Class:#<Array:0x00007fbc862d41b0>>
x.singleton_class.superclass # => Array
This is how you can add methods to individual Ruby objects: you're actually defining them in the object's singleton class.
Fun fact: singleton classes are also objects and thus have their own singleton classes:
x.singleton_class
# => #<Class:#<Array:0x00007fbc869b0060>>
x.singleton_class.singleton_class
# => #<Class:#<Class:#<Array:0x00007fbc869b0060>>>
x.singleton_class.singleton_class.singleton_class
# => #<Class:#<Class:#<Class:#<Array:0x00007fbc869b0060>>>>
This can go as deeply as you want it to.
The reason Ruby doesn't immediately fill up all available RAM with singleton classes and then die is because they are not created until the first time a program uses them. As a result, most objects don't have singleton classes at all.
Isn't this whole singleton class thing kind of overkill?
Not really.
See, Ruby is a language where everything is an object (in the OOP sense of the term), and so this means that classes are also objects. But since all objects have classes, that means each class is also an instance of a class. And so is that class. And this is if we ignore the singleton classes, which we are for the moment.
So how does this end? Well, it's pretty boring actually. Each class
is an instance of the class named Class
, including Class
itself. Class
is an instance of itself and that's all we really
need.
[] # => []
[].class # => Array
[].class.class # => Class
[].class.class.class # => Class
[].class.class.class.class # => Class
But wait! How do we do class methods or class instance variables:
class Thing
def self.instance
@instance = Thing.new unless @instance
return @instance
end
# ...etc...
end
In Smalltalk, this gets done by giving each class object its own
distinct class (the metaclass
) to hold the methods and variable
declarations. They are unnamed but you can get it with the class
method just like Ruby. The metaclass's inheritance tree mirrors the
class's tree (i.e. if Item
is derived from Thing
, then
Thing.class
is derived from Item.class
) with class Class
as the
abstract base class of the heirarchy.
t class. => Thing
t class superclass. => Object
t class superclass superclass. => nil
t class class. => Unnamed class ('Thing class')
t class class superclass. => Unnamed class ('Object class')
t class class superclass superclass. => Class
All metaclasses are instances of the class Metaclass
:
t class. => Thing
t class class. => Unnamed class ('Thing class')
t class class class. => Metaclass
This includes Metaclass
itself, which is how the loop closes:
Metaclass class => 'Metaclass class'
Metaclass class class => Metaclass
(Disclaimer: I've somewhat simplified the above. I also haven't run it.)
In Ruby, each Class
instance (i.e. class) has a singleton class that
holds the class methods and variables. That is, singleton classes
serve as metaclasses. The nice thing about this is that it's a
generalization of what Smalltalk does for classes, and it gives you
instance methods for free.
This is not to say that it's necessarily a better way than Smalltalk's. There are advantages and disadvantages to each approach but I'm far too lazy to write about them here.
# Posted 2021-05-07 01:55:31 UTC; last changed 2021-05-07 01:57:49 UTC