Playing with Ruby object model: prototypal inheritance
The Gang of Four defined the prototype pattern as follows:
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
In other words, the behavior is inherited from specific instances, not classes. The most popular language using this type of inheritance is JavaScript (until the class-based system arrived in ES6). While Ruby uses class-based inheritance, its object model is flexible enough to allow us to implement basic prototypal inheritance.
We're going to implement a T-Rex, starting from a generic animal, through a dinosaur, ending on a specific instance of a Tyrannosaurus.
Defining methods on objects (not classes)
Since Ruby allows to extend classes at runtime (the concept of so-called open classes), it's not surprising that it's possible to extend instances as well. There are three synonymous ways of doing it:
1# by using def…2str1 = "test string"3def str1.big4 upcase + "!"5end67# …which is equivalent to calling def within the instance:8str1.instance_eval do9 def big10 upcase + "!!"11 end12end1314# by using define_singleton_method15str2 = "test string"16str2.define_singleton_method :big do17 upcase + "!"18end1920# by extending the eigenclass/singleton class21str3 = "test string"22class << str323 def big24 upcase + "!"25 end26end2728str1.big29# => "TEST STRING!!"30str2.big31# => "TEST STRING!"32str3.big33# => "TEST STRING!"
All three ways do the same thing, but the third form gives a hint on how Ruby handles adding a method to a specific object: it creates an “anonymous” class, inserts the method to it and prepends this class to the object's inheritance chain. This class is called eigenclass, or ghost class, or singleton class.
You can easily verify it: calling str1.singleton_methods
, str2.singleton_methods
, str3.singleton_methods
will return [:big]
.
If you'd like to learn more about singleton classes, I highly recommend reading Ruby’s Anonymous Eigenclass: Putting the “Ei” in Team.
Back to the prototypal inheritance
Let's start by implementing a generic animal prototype.
1animal = Object.new2def animal.taxonomic_name3 "Animalia"4end56def animal.breathe7 puts "[inhales] [exhales]"8end910def animal.__proto__11 nil12end1314def animal.taxonomic_rank15 __proto__ ? (__proto__.taxonomic_rank + [taxonomic_name]).uniq : [taxonomic_name]16end
Notice how we started by creating an empty object and continued by defining methods on that specific instance. We can also see a hint of the things to come: our objects will store the prototype inside __proto__
method and will determine the animal's taxonomic rank by traversing the prototype chain.
Our animal kingdom needs a constructor method. Unsurprisingly, we'll call it new
:
1def animal.new2 prototype_obj = self3 new_obj = clone4 new_obj.define_singleton_method :__proto__ do5 prototype_obj6 end7 new_obj8end
You may wonder: why did we use define_singleton_method
instead of any other form? Well, we wanted __proto__
method to return the value of self
from the prototype's new
method. However, using def
/class
switches the lexical scope, meaning that any values previously defined in the block are not visible in the body of the method we're defining using the two keywords. Fortunately, closures “remember” the local values from the scope they were defined in, so using define_singleton_method
with a block (closure) allows us to access the value of prototype_obj
inside the method we're defining.
Let's test our implementation by creating a dinosaur from the animal prototype:
1dinosaur = animal.new2def dinosaur.taxonomic_name3 "Dinosauria"4end56def dinosaur.walk7 puts "[walks]"8end910def dinosaur.run11 puts "[walks faster]"12end1314dinosaur.breathe15# [inhales] [exhales]16dinosaur.run17# [walks faster]18dinosaur.taxonomic_rank19# => ["Animalia", "Dinosauria"]20animal.run21# NoMethodError (undefined method `run' for #<Object:0x00007fb0780de618>)
So far, so good – our inheritance works correctly. Since we defined the ability to run on the dinosaur, a generic animal cannot do it, and attempt to do so raises the expected exception.
Now let's finalize the chain by defining a theropod, tyrannosaurus and a specific instance of T-Rex:
1theropod = dinosaur.new2def theropod.taxonomic_name3 "Theropoda"4end5def theropod.run6 puts "[runs pretty fast]"7end89tyrannosaurus = theropod.new10def tyrannosaurus.taxonomic_name11 "Tyrannosaurus"12end1314t_rex = tyrannosaurus.new15t_rex.run16# [runs pretty fast]17t_rex.breathe18# [inhales] [exhales]19t_rex.taxonomic_rank20# => ["Animalia", "Dinosauria", "Theropoda", "Tyrannosaurus"]
Works as expected.
Finally, let's pretend that our implementation of dinosaurs is class-based:
1Animal = animal2Dinosaur = dinosaur3Theropod = theropod4Tyrannosaurus = tyrannosaurus56# ...78t = Tyrannosaurus.new9t.taxonomic_rank10# =>["Animalia", "Dinosauria", "Theropoda", "Tyrannosaurus"]
Without reading the implementation details, nobody would think that this is not a class-based system.
What are classes in Ruby anyway?
It's often repeated that in Ruby everything is an object. No Rubyist should be surprised that 5.class
returns Integer
and that we can represent blocks of code as Proc
objects. Even methods are objects (and operators are just methods):
15.0.method(:round).class2# => Method35.0.method(:+).class4=> Method
What about classes? They are objects, too!
1Class.class2# => Class3Class.class.class4# => Class5Class.ancestors6# => [Class, Module, Object, Kernel, BasicObject]7A = Class.new do8 def test_method9 "Test"10 end11end12B = Class.new(A)13B.ancestors14# => [B, A, Object, Kernel, BasicObject]15B.new.test_method16# => "Test"
Wrapping up
Is prototypal inheritance any useful in Ruby? Probably not. Some people would say that skipping expensive constructors in favor of fast object cloning or opting out of costly method lookup in the inheritance chain and using local methods instead (cloned from the prototype) can have performance benefits, but that is a weak argument for any high-level language. Still, playing with the object model by implementing an un-Ruby idiom is an excellent opportunity to systematize more advanced concepts about this language.