True emptyness in Ruby
After learning about BasicObject class and the almost blank state object it can produce, I was wondering if it's possible to have an even smaller object. And the answer is yes, with a bit of metaprogramming.
Among Class instance methods, there's remove_method and undef_method. The former removes a method definition from the object, but allows it to search for any inherited methods with the same name. The later, however, goes nuclear and completelly removes any references, period.
So, in order to produce a completely empty object in Ruby, we have to create a class that doesn't add any instance methods to it's instances, which also includes initialize .
First, as I discovered before, one can simply allocate an object in memory, no need to call it's initializer.
Second, right before allocating the object in memory, we also have to undefine all methods that could be defined into the object during instantiation.
BasicObject has a total of 8 public instance methods:
BasicObject.public_instance_methods
=> [:equal?, :!, :__send__, :==, :!=, :instance_eval, :instance_exec, :__id__]
And 5 private instance methods, with initialize included:
BasicObject.private_instance_methods
=> [:initialize, :method_missing, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined]
There's also protected_instance_methods, which is empty in our current state. Since we are not the only ones capable of metaprogramming - meaning other people can monkeypatch new public, protected and private methods into BasicObject - we'll call undef_method to all three.
Thus, our class will be:
class O < BasicObject
private_instance_methods.each{ |_| undef_method(_) }
protected_instance_methods.each{ |_| undef_method(_) }
public_instance_methods.each{ |_| undef_method(_) }
def self.new
_ = allocate
_
end
end
When this code is read by the interpreter, we get a couple of warnings, rightly so:
(irb):3: warning: undefining `initialize' may cause serious problems
(irb):5: warning: undefining `__send__' may cause serious problems
Ignore that.
We successfuly defined a class that can produce the most empty object possible in Ruby. Also, it's worth noting that Ruby seems to keep track of the instance methods forcefully undefined, as we can check in O 's undefined_instance_methods:
O.public_instance_methods
=> []
O.protected_instance_methods
=> []
O.private_instance_methods
=> []
O.undefined_instance_methods
=> [:equal?, :!, :__send__, :initialize, :==, :!=, :method_missing, :singleton_method_added, :instance_eval, :singleton_method_removed, :instance_exec, :singleton_method_undefined, :__id__]
Now, to achieve true emptyness in Ruby, we simply call:
o = O.new
=> #<O:0x00007f010e5e76d8>
Beautiful.
We could say object o is useless as a piece of working code, but it doesn't care. Because we started from BasicObject, we already ditched methods such as respond_to? , methods or inspect , so it won't bother telling us what it can or cannot do.
o does not know about it's ancestrality:
o.class
=> "<main>: undefined method `class' for an instance of O (NoMethodError)"
o will not compare itself with others:
common_obj = Object.new
=> #<Object:0x00007f010e5fa5f8>
o == common_obj
=> "<main>: undefined method '==' for an instance of O (NoMethodError)..."
o != common_obj
=> "<main>: undefined method `!=' for an instance of O (NoMethodError)..."
o.equal?(common_obj)
=> "<main>: undefined method `equal?' for an instance of O (NoMethodError)..."
o does not bear an identity:
o.__id__
=> "<main>: undefined method `__id__' for an instance of O (NoMethodError)"
But won't negate it's own existence either:
!o
=> "<main>: undefined method `!' for an instance of O (NoMethodError)"
We can't ask o to do or learn new things on it's own volition:
o.instance_eval do
def speak
"..."
end
end
=> "<main>: undefined method `instance_eval' for #<O:0x00007f010e5e76d8> (NoMethodError)"
Even nil, commonly used by us Ruby developers to represent the concept of "nothing", is actually something, while o is not:
nil.class
=> NilClass
nil.class.superclass
=> Object
nil.respond_to?(:to_s)
=> true
nil == o
=> false
As far as Ruby interpreter's concern, o is nothing but allocated memory.
o is perfect.
Is it, though?
Trully, o cannot exist without it's class definition, even if it doesn't know about it. Internally, Ruby still keeps that reference somehow, as we can see when o is returned:
#<O:0x00007f010e5e76d8>
Another example: if we marshal o and then try to load it in a separate irb instance:
Marshal.dump(o)
=> "\x04\bo:\x06O\x00"
# Exit irb and enter it back again:
Marshal.load("\x04\bo:\x06O\x00")
=> <internal:marshal>:34:in `load': undefined class/module O (ArgumentError)
It cannot exist, because our class O was never defined in this new console instance.
Which prompts the question: can objects in Ruby be even more empty than this?
A word on corruption
Of course, we can exert force upon o, make it go against it's own nature. It can be reintroduced to mundane concepts by the means of a singleton method.
As an example, we can give o an identity:
def o.__id__
0
end
=> "<main>: undefined method `singleton_method_added' for #<O:0x00007f010e5e76d8> (NoMethodError)"
o.__id__
=> 0
The error shown happens because o does not provide a .singleton_method_added callback to be called upon, but that doesn't prevent our object from actually learning the new method. In order to quiet such dissident voices, we can either redefine .singleton_method_added into o or simply rescue the block and do nothing. From now on, let's just rescue and ignore.
Now, we can reinstante existential pain into o by teaching it to compare itself:
begin
def o.equal?(obj)
obj == self
end
rescue; end
=> nil
o.equal?(Object.new)
=> false
o is now burdened by knowledge.