Object oriented programming with Luart

By Samir Tine, published on September 2022

Lua is a multi-paradigm programming language. It supports different programming styles. One of the popular style used when coding is by creating objects. This is known as Object-Oriented Programming.

Lua supports the use of OOP but with some efforts, dealing with metatables, which is not very beginner friendly. Luart provides OOP out of the box, and its runtime libray relies a lot on it. This tutorial will cover OOP programming with Luart

Object

In Luart, Objects are specialized tables, made up of fields and methods. The difference with a Lua table is that an object makes it possible to create (or rather "instantiate") a value (called "instance") from this object, i.e. made up of the same fields and methods.

Let's define our first object :

-- Define a Polygon empty object Polygon = Object {} -- Prints 'Object: 08BDEF' print(Polygon) -- Prints 'Object' print(type(Polygon)) -- Add a field 'sides' to the Polygon Object Polygon.sides = 0

This example defines an emptyPolygonobject. It's quite useless for the moment, but you can see that we use an Object() function to define it, and that an Object has a type of "Object". Objects are dynamic, as Lua tables, and you can add fields after their definition. Here we are adding a field named 'sides' with a value of 0.

The field sides can also be set during thePolygonObject definition :

-- Define a Polygon object with a field 'sides' Polygon = Object { sides = 0 } -- Prints the number 0 print(Polygon.sides) -- Prints nil, the field don't exists print(Polygon.area)

An Object, as you can see, has the same behaviour than a standard Lua table. For now, there's really no point in using an Object instead of a table. Let's go a little further to understand the interest of programming with objects.

Instances

An Object act as a "prototype", or a "model" to create values from it. All the values of an Object share the same fields and methods. To create a new value (also called an instance) of an Object, use the following syntax :

-- Create a new instance of Polygon Object square = Polygon() -- Prints the number 0, from the Polygon.sides field print(square.sides) -- Change Polygon.sides field (a square has 4 sides !) Polygon.sides = 4 -- Prints the number 4, from the Polygon.sides field print(square.sides)

To create a new instance of a Polygon, we use the call notation with parenthesis (like a function call), which then returns a new value of a Polygon. As you can see, all fields and methods of an Object are shared with its instances. So the field sides, which is not defined in the instance, is retrieved from Polygon. Changing the Polygon.sides value will then affect all of its instances.

Let's create another instance from Polygon :

-- Create a new instance of Polygon Object hexagon = Polygon() -- Prints the number 4, from the Polygon.sides field print(hexagon.sides) -- Change Polygon.sides field (a hexagon has 6 sides !) Polygon.sides = 6 -- Great, prints the number 6 ! print(hexagon.sides) -- Ouch, prints the number 6 too ! print(square.sides)

Objects fields are common to all of their instances, and therefore, are not well suited for specific instance behaviour. In this case, changing the Polygon.sides value, will affect all of its instances (square and hexagon).

The solution is simple, we will define this field in each instance rather than at the level of their parent Object :

-- Define an empty Polygon Object Polygon = Object {} -- Create a new instance 'square' and set its field 'sides' square = Polygon() square.sides = 4 -- Create a new instance 'hexagon' hexagon = Polygon() hexagon.sides = 6

This step actually consists of customizing (or initializing) each instance. And this is where the constructor concept gets interesting.

Constructor

Do you remember the process of creating an instance ? When we called the Polygon Object like a function ? In fact, what we called was not the Object itself, but a specific internal function called a constructor.

When defined, the constructor method of an object is used to initialize instances. Let's create one for the Polygon object :

-- Define an empty Polygon Object Polygon = Object {} -- Define the Polygon constructor function Polygon:constructor() print("A new instance of Polygon has been created !") end -- Create a new instance of Polygon (prints the message above too !) square = Polygon()

Each time a new instance will be created, the Polygon.constructor method will be called, printing a message which is not very helpful I have to admit.

The interest of the constructor is in fact to be able to personalize/initialize the instance during its creation :

-- Define an empty Polygon Object Polygon = Object {} -- Define the Polygon constructor with one argument function Polygon:constructor(n) -- set the field 'sides' from the self instance to n self.sides = n end -- Create a new instance of Polygon with 4 as constructor argument square = Polygon(4) -- Great, a square is a Polygon with 4 sides ! print(square.sides)

During each Polygon instance creation, the constructor method sets a field sides in the instance (represented by the self variable). This initializes the instance with the parameters provided to the constructor.

We can now create new instances with a personalized sides field :

-- Create a new instance of Polygon with 6 as constructor argument hexagon = Polygon(6) -- Great, a hexagon is a Polygon with 6 sides ! print(hexagon.sides) -- Create a new instance of Polygon with 8 as constructor argument octagon = Polygon(8) -- Great, an octagon is a Polygon with 8 sides ! print(octagon.sides)

Before calling the constructor method, internally Luart creates the new instance, and set it as the first argument of the constructor (the famous self). The Object constructor, as any other method in Lua, can then be written in two different ways :

-- First method, with the ':' notation (implicit self) function Polygon:constructor(n) self.sides = n end -- Second method, with the '.' notation (explicit self) function Polygon.constructor(self, n) self.sides = n end

Remember that Objects are like Lua tables, so here is another method to define an Object constructor:

-- Third method, like a table key = value Polygon.constructor = function (self, n) self.sides = n end

Using this method of constructor definition makes it necessary to use the explicit notation with self.

Object constructors are an essential part of object-oriented programming. They allow you to create instances of a given Object and properly initialize them to be ready to use.

By now you're ready to take full control over the instance creation process using Objects constructors !