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
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 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.

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 new:

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 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:

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 5.class returns 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"

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.

Posted on January 26, 2019 in ruby by Jacek Galanciak

comments powered by Disqus