Playing with Ruby object model: prototypal inheritance

Jacek Galanciak

January 26, 2019

ruby

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.big
4 upcase + "!"
5end
6
7# …which is equivalent to calling def within the instance:
8str1.instance_eval do
9 def big
10 upcase + "!!"
11 end
12end
13
14# by using define_singleton_method
15str2 = "test string"
16str2.define_singleton_method :big do
17 upcase + "!"
18end
19
20# by extending the eigenclass/singleton class
21str3 = "test string"
22class << str3
23 def big
24 upcase + "!"
25 end
26end
27
28str1.big
29# => "TEST STRING!!"
30str2.big
31# => "TEST STRING!"
32str3.big
33# => "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.new
2def animal.taxonomic_name
3 "Animalia"
4end
5
6def animal.breathe
7 puts "[inhales] [exhales]"
8end
9
10def animal.__proto__
11 nil
12end
13
14def animal.taxonomic_rank
15 __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.new
2 prototype_obj = self
3 new_obj = clone
4 new_obj.define_singleton_method :__proto__ do
5 prototype_obj
6 end
7 new_obj
8end

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.new
2def dinosaur.taxonomic_name
3 "Dinosauria"
4end
5
6def dinosaur.walk
7 puts "[walks]"
8end
9
10def dinosaur.run
11 puts "[walks faster]"
12end
13
14dinosaur.breathe
15# [inhales] [exhales]
16dinosaur.run
17# [walks faster]
18dinosaur.taxonomic_rank
19# => ["Animalia", "Dinosauria"]
20animal.run
21# 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.new
2def theropod.taxonomic_name
3 "Theropoda"
4end
5def theropod.run
6 puts "[runs pretty fast]"
7end
8
9tyrannosaurus = theropod.new
10def tyrannosaurus.taxonomic_name
11 "Tyrannosaurus"
12end
13
14t_rex = tyrannosaurus.new
15t_rex.run
16# [runs pretty fast]
17t_rex.breathe
18# [inhales] [exhales]
19t_rex.taxonomic_rank
20# => ["Animalia", "Dinosauria", "Theropoda", "Tyrannosaurus"]

Works as expected.

Finally, let's pretend that our implementation of dinosaurs is class-based:

1Animal = animal
2Dinosaur = dinosaur
3Theropod = theropod
4Tyrannosaurus = tyrannosaurus
5
6# ...
7
8t = Tyrannosaurus.new
9t.taxonomic_rank
10# =>["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).class
2# => Method
35.0.method(:+).class
4=> Method

What about classes? They are objects, too!

1Class.class
2# => Class
3Class.class.class
4# => Class
5Class.ancestors
6# => [Class, Module, Object, Kernel, BasicObject]
7A = Class.new do
8 def test_method
9 "Test"
10 end
11end
12B = Class.new(A)
13B.ancestors
14# => [B, A, Object, Kernel, BasicObject]
15B.new.test_method
16# => "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.

Jacek Galanciak © 2023