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.
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # by using def… str1 = "test string" def str1.big upcase + "!" end # …which is equivalent to calling def within the instance: str1.instance_eval do def big upcase + "!!" end end # by using define_singleton_method str2 = "test string" str2.define_singleton_method :big do upcase + "!" end # by extending the eigenclass/singleton class str3 = "test string" class << str3 def big upcase + "!" end end str1.big # => "TEST STRING!!" str2.big # => "TEST STRING!" str3.big # => "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
str3.singleton_methods will return
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 animal = Object.new def animal.taxonomic_name "Animalia" end def animal.breathe puts "[inhales] [exhales]" end def animal.__proto__ nil end def animal.taxonomic_rank __proto__ ? (__proto__.taxonomic_rank + [taxonomic_name]).uniq : [taxonomic_name] end
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
1 2 3 4 5 6 7 8 def animal.new prototype_obj = self new_obj = clone new_obj.define_singleton_method :__proto__ do prototype_obj end new_obj end
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
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 dinosaur = animal.new def dinosaur.taxonomic_name "Dinosauria" end def dinosaur.walk puts "[walks]" end def dinosaur.run puts "[walks faster]" end dinosaur.breathe # [inhales] [exhales] dinosaur.run # [walks faster] dinosaur.taxonomic_rank # => ["Animalia", "Dinosauria"] animal.run # 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 theropod = dinosaur.new def theropod.taxonomic_name "Theropoda" end def theropod.run puts "[runs pretty fast]" end tyrannosaurus = theropod.new def tyrannosaurus.taxonomic_name "Tyrannosaurus" end t_rex = tyrannosaurus.new t_rex.run # [runs pretty fast] t_rex.breathe # [inhales] [exhales] t_rex.taxonomic_rank # => ["Animalia", "Dinosauria", "Theropoda", "Tyrannosaurus"]
Works as expected.
Finally, let's pretend that our implementation of dinosaurs is class-based:
1 2 3 4 5 6 7 8 9 10 Animal = animal Dinosaur = dinosaur Theropod = theropod Tyrannosaurus = tyrannosaurus # ... t = Tyrannosaurus.new t.taxonomic_rank # =>["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
Integer and that we can represent blocks of code as
Proc objects. Even methods are objects (and operators are just methods):
1 2 3 4 5.0.method(:round).class # => Method 5.0.method(:+).class => Method
What about classes? They are objects, too!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Class.class # => Class Class.class.class # => Class Class.ancestors # => [Class, Module, Object, Kernel, BasicObject] A = Class.new do def test_method "Test" end end B = Class.new(A) B.ancestors # => [B, A, Object, Kernel, BasicObject] B.new.test_method # => "Test"
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.comments powered by Disqus