Object oriented progamming: inheritance and mixins

By Samir Tine, published on January 2023

Inheritance

Inheritance allows an object to reuse the logic of another existing object. This way, it will inherit all attributes, methods and properties that were previously defined. Let's go back to our Fruct object. Let's imagine that we want to define a new TropicalFruct object. The simplest way is to use the Fruct object as the basis for defining this new object by proceeding as follows:

TropicalFruct = Object(Fruct)

We define a new object TropicalFruct but this time, we will use the Fruct object as parameter, instead of an empty table. This indicates that we are relying on the Fruct object to define the new object, thus retrieving all of its attributes, methods, and properties. Let's check that TropicalFruct has the same behaviour than the Fruct object :

-- The Fruct:constructor() method works ! pineapple = TropicalFruct("pine apple", "lightbrown", "ovoid") -- prints "pine apple" => the get_name() getter method property works ! print(redapple.name) -- error => the set_name() setter method property works ! redapple.name = "banana" -- prints "I am a pine apple, a lightbrown and ovoid fruct" pineapple:describe()

With Luart, an object can have only one single inherited object. But the later can inherits another parent object, and so one... It's a single multiinheritance mechanism. In this case, we have the following single inheritance diagram :

Ancestors diagram

Remember that inheritance can be checked using the is() global function, it not only works with instances but also with objects :

is(TropicalFruct, Fruct) -- true is(Fruct, TropicalFruct) -- false is(pineapple, TropicalFruct) -- true is(pineapple, Fruct) -- true, pineapple is a TropicalFruct, which in turn is a Fruct too is(redapple, TropicalFruct) -- false, redapple is a Fruct, not a TropicalFruct (which is more specialized/redefined)

Method overriding

That's great, everthing we wrote so far can be reused for our new object ! it's the concept of code reusability. What is very interesting is that we can always modify the attributes and methods of this new object, without modifying the ancestor object (also called parent object). Let's modifiy the describe method :

function TropicalFruct:describe() print("I am a "..self.name..", a "..self.color.." and "..self.shape.." tropical fruct") end

We just customized this method to better match our new object. This customization is called "method overriding". It is indeed sometimes preferable to adapt a method for a new object. This new definition overrides the previous one inherited from the parent object, hence the name "overriding", but the original definition of the parent object persists and is not impacted :

redapple = Fruct("red apple", "red", "spherical") redapple:describe() -- prints "I am a red apple, a red and spherical fruct" pineapple = TropicalFruct("pine apple", "lightbrown", "ovoid") pineapple:describe() -- prints "I am a pine apple, a lightbrown and ovoid tropical fruct"

Another way to change the message printed using the describe() method when overriding is to call the original method from the Fruct object :

function TropicalFruct:describe() Fruct.describe(self) -- prints "I am a pine apple, a lightbrown and ovoid fruct" print("And I'm from the tropics") end

Here we call the Fruct.describe() method directly, but using the current implicit self (which is the current instance of TropicalFruct). We can also use the special global function super() the get the same result :

function TropicalFruct:describe() super(self).describe(self) -- prints "I am a pine apple, a lightbrown and ovoid fruct" print("And I'm from the tropics") end
See you how can use the super() function to call an ancestor method. Please note the use of the "." dot notation.
Method call using ":" semicolon is not possible as super() returns an Object, not an instance value.

Mixins

Fruit is good but not very consistent. Now let's try to create an object of type Meat (sorry to our vegetarian friends!) :

Meat = Object {}

Now let's add an "eat" behavior to our object by adding the following method:

function Meat:eat() print("I am eating meat") end beef = Meat() beef:eat() -- prints "I am eating meat"

Ok it works as expected. But what if we try to set the same "eat" behavior to our Fruct object? The easiest way would be to add a new eat() method to the Fruct object. What if we use the same eat() method from the Meat object again? To do this, the concept of Mixins comes to our aid:

-- first we define a simple Lua table local eat_mixin = {} -- then we define our eat() method -- we change our message to use the current object name function eat_mixin:eat() print("I am eating "..type(self)) end -- now we use it when defining Fruct and Meat objects Fruct = Object({}, eat_mixin) Meat = Object({}, eat_mixin) beef = Meat() beef:eat() -- prints "I am eating Meat" apple = Fruct() apple:eat() -- prints "I am eating Fruct"

Mixins are tables of fields and methods used to enhance and extend the functionality of an object. Basically, it lets us group the behaviors that we want to apply to objects. It's like the concept of interfaces in other programming languages.

The Object() function allows you to define any number of mixins you want.