Skip to content

Lua Object Oriented

Object-Oriented Programming (OOP) is a paradigm based on the concept of “objects”, which can contain data in the form of fields (often known as attributes or properties) and code in the form of procedures (often known as methods). Key OOP features include:

  • Objects: Instances that bundle state (data) and behavior (methods).
  • Classes: Blueprints or templates for creating objects (less direct in Lua, which is prototype-based).
  • Encapsulation: Bundling data and methods that operate on the data within one unit, and restricting direct access to some of an object’s components.
  • Inheritance: A mechanism where new classes (or objects in Lua) derive properties and methods from existing ones.
  • Polymorphism: The ability of an object to take on many forms, typically allowing methods to do different things based on the object it is acting upon.

Lua does not have built-in classes in the traditional sense like Java or C++. Instead, OOP in Lua is typically implemented using tables and metatables, following a prototype-based approach. Objects inherit directly from other objects (their prototypes).

  • Objects as Tables: A Lua object is usually represented by a table. Its fields store the object’s state, and its methods are functions stored in the table (or its prototype).
  • Prototypes and __index: Inheritance is achieved by setting a table’s metatable and using the __index metamethod to point to its prototype. If a field or method is not found in an object, Lua looks it up in the prototype via __index.
  • self convention: Methods are often defined and called using the colon operator (:), which implicitly passes the object itself as the first argument, conventionally named self.

Let’s create a ‘class’ (prototype table) for a Rectangle.

-- Rectangle.lua (our prototype or 'class' table)
local Rectangle = {}
Rectangle.__index = Rectangle -- Key for inheritance: methods looked up in Rectangle itself
-- Constructor function (conventionally 'new')
function Rectangle:new(initial_length, initial_breadth)
local o = {} -- Create a new empty table (the instance)
setmetatable(o, self) -- Set its metatable to Rectangle (or a derived class)
-- self here will be Rectangle when called as Rectangle:new()
o.length = initial_length or 0
o.breadth = initial_breadth or 0
return o
end
-- Method to calculate area
function Rectangle:calculateArea()
return self.length * self.breadth
end
-- Method to print area
function Rectangle:printArea()
print("The area of the rectangle is: ", self:calculateArea())
end
-- To make this a module, you would typically return the Rectangle table
-- return Rectangle

To use our Rectangle ‘class’, we first need to ensure it’s loaded (e.g., via require if it’s in a separate file, or just defined in the same scope).

-- Assuming Rectangle definition from above is available
-- Create an instance (object) of Rectangle
local rect1 = Rectangle:new(10, 5)
-- Access properties
print("Rectangle 1 Length:", rect1.length) -- Output: 10
print("Rectangle 1 Breadth:", rect1.breadth) -- Output: 5
-- Call methods
rect1:printArea() -- Output: The area of the rectangle is: 50
local rect2 = Rectangle:new(7, 3)
rect2:printArea() -- Output: The area of the rectangle is: 21

How self and : work:

  • When you define function Rectangle:new(...), it’s syntactic sugar for Rectangle.new = function(self, ...).
  • When you call rect1:printArea(), it’s syntactic sugar for rect1.printArea(rect1).

Let’s first define a more generic Shape prototype and then have Square inherit from it.

-- Shape.lua (Base prototype)
local Shape = {}
Shape.__index = Shape
function Shape:new()
local o = {}
setmetatable(o, self)
return o
end
function Shape:getType()
return "Generic Shape"
end
-- Square.lua (Derived prototype)
local Square = Shape:new() -- Create Square object, inheriting from Shape initially
-- (or Square = setmetatable({}, Shape) then Square.__index = Square for methods specific to Square)
Square.__index = Square -- Methods not in instance look in Square, then Shape via metatable chaining
setmetatable(Square, Shape) -- Square's metatable is Shape, so Square 'is-a' Shape prototype
-- Square's own constructor
function Square:new(side)
local o = Shape.new(self) -- Call Shape's 'new' but ensuring metatable is set to Square
-- A more robust way: local o = {}; setmetatable(o, self)
o.side = side or 0
return o
end
-- Override Shape's getType method
function Square:getType()
return "Square"
end
-- Square-specific method
function Square:calculateArea()
return self.side * self.side
end
function Square:printArea()
print("The area of the square (side " .. self.side .. ") is: " .. self:calculateArea())
end
-- Example Usage:
local generic_shape = Shape:new()
print(generic_shape:getType()) -- Output: Generic Shape
local my_square = Square:new(7)
print(my_square:getType()) -- Output: Square (overridden method)
my_square:printArea() -- Output: The area of the square (side 7) is: 49
-- Demonstrating inheritance chain for methods not in Square itself:
-- If Square had a metatable like { __index = Shape } and Square.getType was nil,
-- my_square:getType() would resolve to Shape:getType().
-- Our current setup Square.__index = Square; setmetatable(Square, Shape) achieves this for 'class' methods
-- and instance methods are resolved via instance_mt.__index = Square.

Explaining the inheritance for Square:

  1. local Square = Shape:new() or more clearly Square = {}.
  2. setmetatable(Square, { __index = Shape }). This makes Square inherit from Shape. If a key is not in Square, Lua looks in Shape.
  3. Square.__index = Square. For instances of Square, if a key is not in the instance, Lua looks in the Square table itself.
  4. The constructor Square:new(side): Creates an instance o, sets its metatable to Square (so o inherits from Square), and initializes its specific properties.
A more common pattern for `Square:new` related to `Shape:new`:
-- More robust Square constructor, assuming Shape has a constructor Shape:new(o_template)
-- Shape.lua
-- ... (Shape prototype as before)
-- function Shape:new(o_template)
-- o_template = o_template or {}
-- setmetatable(o_template, self)
-- self.__index = self -- ensure instances lookup methods in their class prototype
-- return o_template
-- end
-- Square.lua
-- ... (Square prototype setup, Square.__index = Square, setmetatable(Square, {__index = Shape}))
-- function Square:new(side)
-- local o = Shape:new(self) -- 'self' here is Square, so Shape:new correctly sets metatable for Shape part
-- -- However, o's metatable is now Shape. We need it to be Square.
-- -- A better way:
-- local o = {}
-- setmetatable(o, self) -- self is Square, so o's metatable points to Square
-- Shape.initialize(o) -- if Shape had an initialize method (optional)
-- o.side = side or 0
-- return o
-- end
-- The simplest pattern for Square inheriting Rectangle (from first example):
-- Rectangle prototype as defined initially
-- Square prototype
local Square = {}
setmetatable(Square, {__index = Rectangle}) -- Square inherits from Rectangle
Square.__index = Square -- Instances of Square look up methods in Square first
function Square:new(side)
local o = {}
setmetatable(o, self) -- self is Square table when called as Square:new()
o.length = side -- Use fields expected by Rectangle if reusing its methods
o.breadth = side
-- o.side = side -- Or use its own field
return o
end
-- Optionally override methods
function Square:printArea()
-- If using .side:
-- print("The area of the square is: ", self.side * self.side)
-- If reusing Rectangle's calculateArea via inherited fields:
print("The area of the square is: ", Rectangle.calculateArea(self))
end
local sq = Square:new(4)
sq:printArea() -- Will call Square's printArea
-- To call Rectangle's calculateArea if it wasn't shadowed:
-- print(Rectangle.calculateArea(sq))

Practical Application: OOP in Lua is used extensively in game development for character entities, UI elements, and game systems. It’s also used in larger applications for structuring code into manageable and reusable components.

Further Learning: Explore concepts like mixins, multiple inheritance (can be emulated with __index functions), and different OOP implementation patterns in Lua. The book “Programming in Lua” by Roberto Ierusalimschy provides excellent chapters on this topic.