Inheritance with Luart

By Samir Tine, published on September 2022

Inheritance refers to the process of creating new objects from existing ones. Thus, we obtain a hierarchy of objects. All fields, properties, and behaviors of the parent object are acquired by a child object created through inheritance.

Luart Objects supports single inheritance (parent and child object), and multilevel inheritance (grandparent, parent, child object and so on...).

Single inheritance

In the previous OOP tutorial, you already used single inheritance without even knowing it :

-- Define a Polygon empty object, inherited from an empty table Polygon = Object {}

You already know that this example defines an emptyPolygonobject. Thanks to the Object() function, which permits to define it, with an empty table as argument.

Let's try to provide a non empty table this time :

-- Let's define an 'ancestor' table ancestor = { sides = 0 } -- Define a Polygon object inheriting from 'ancestor' Polygon = Object(ancestor) -- Prints the number 0 print(Polygon.sides) ancestor.sides = 4 -- Prints the number 4 print(Polygon.sides)

In this case, the Polygon objects inherits the field sides from its parent table. Hence, changing a parent field value will impact all other Objects that inherits from it.

Let's try to define the Polygon object with another Object as ancestor :

-- Define a 'Figure' Object with a single field 'name' Figure = Object { name = "Figure" } -- Define a Polygon object inheriting from 'Figure' Polygon = Object(Figure) -- Add a new field to the Polygon object Polygon.sides = 4 -- Prints the string "Figure" print(Polygon.name) -- Prints the number 4 print(Polygon.sides)

In this example, the Polygon Object inherits the field name from its ancestor Figure, and defines a new field sides.

Polygon is like a specialized Figure, inheriting its fields, but defining new ones which are specific to it. This means that inheritance supports code reusability.

Figure can be called the parent Object, or the ancestor Object, and event the super Object. Here is how to get the ancestor of an object :

-- Prints true print(super(Polygon) == Figure) -- Prints true print(super(Figure) == nil)

To get an Object ancestor, you can use the super() function. If the Object has no Object ancestor, it returns nil.

To test if an Object inherits from another, you can use a specific is() function :

-- Prints true print(is(Polygon, Figure))

Let's go a little further with inheritance by defining a Figure constructor to set the name field upon instance creation :

-- Define a Figure constructor function Figure:constructor(name) self.name = name end -- Create an instance of Polygon square = Polygon("I'm a Polygon") -- Prints "I'am a Polygon" print(square.name)
-- Define a Figure constructor function Figure:constructor(name) self.name = name end -- Create an instance of Polygon square = Polygon("I'm a Polygon") -- Prints "I'am a Polygon" print(square.name)

In this example, when creating the square Polygon instance, the Figure inherited constructor is used, as the Polygon constructor is not defined. The field square.name is set with thr provided string argument.

Let's see what happens if we define a Polygon constructor :

-- Define a Polygon constructor function Polygon:constructor(n) self.sides = n end -- Create an instance of Polygon square = Polygon(4) -- Ouch, prints "Figure" ! print(square.name)

This time, as Polygon provides a constructor, the Figure constructor is not inherited (it's called overloading, a kind of polymorphism). That's why the square.name field use the inherited string "Figure".

Now let's see how to fix it :

-- Define a Polygon constructor function Polygon:constructor(n) self.sides = n -- Call the Figure constructor with our instance as self parameter Figure.constructor(self, "I am a Polygon !") end -- Create an instance of Polygon square = Polygon(4) -- Great, prints "I am a Polygon !" print(square.name)

With this new Polygon constructor, we call directly the ancestor's constructor, using the self instance as first parameter (with the dot notation).

Here is another way to achieve this :

-- Define a Polygon constructor function Polygon:constructor(n) self.sides = n -- Call the ancestor constructor with our instance as self parameter super(self).constructor(self, "I am a Polygon !") end

This method is more elegant and universal, as you may not always know what is the ancestor of an Object.

Tips

Be sure with the super() function to use only the dot notation when calling ancestor methods as it returns Objects, not instances.

Multilevel inheritance

Let's continue to gradually move towards a slightly more complex notion. This time, we will create a new Object inherated from Polygon :

-- Define a Square Object inherited from Polygon Square = Object(Polygon) -- Define the Square constructor function Square:constructor(sidelength) self.sidelength = sidelength -- call the Polygon constructor with parameter 4 (the number of sides) super(self).constructor(self, 4) end -- Create a new instance of Square big_square = Square(100) -- Prints "I am a Polygon !", inherited from Polygon which inherits from Figure print(big_square.name) -- Prints 4, inherited from Polygon print(big_square.sides) -- Prints 100, not inherited print(big_square.sidelength)

This is multilevel inheritance : The Square Object inherits from Polygon which inherits from Figure. Each time you use a field or method which is not defined in the current instance, the field is retrieved from the ancestor Object, and if not found, to the ancestor of the ancestor Object, and so on...

The interest of multilevel inheritance is to reuse the code already written in ancestors Objects. By adding a new behavior to an ancestor, all childs Objects will benefit. Let's do it :

-- Redefine the Figure constructor function Figure:constructor(name) self.name = "Figure name is '"..name.."'" end -- Prints "My name is 'I am a Polygon !'" print(big_square.name)

By changing the Figure constructor, we changed the behaviour of its childs Objects Polygon and Square. Rather simple !

The inheritance relationship between objects provides a hierarchical structure. In real life, we have similar relationships. That's why object-oriented programming is simple to use and understand, and I hope it's also clear to you now !