Getting Classy With Inheritance In Lua

We’re going to cover the basics of inheritance in Lua. Lua doesn’t have native classes, so don’t expect things like multiple inheritance or similar to be practical. I basically have never used inheritance in Lua in any serious capacity outside of very few specific situations.

We’re going to cover basic inheritance using the default “class system” in Lua, how to add functions, and how to override functions. There are technically ways to do multiple inheritance, but at that point, it would be best to use a library made for classes or approach the problem differently. Using too heavy of an object-oriented approach in a language which isn’t built for it will tend to make code sloppier and less reliable unless you know exactly what you’re doing, and why.

What Is Inheritance?

Inheritance is an object-oriented concept which defines a class deriving from another class. All squares are rectangles, but not all rectangles are squares. A rectangle is a shape, and so is a square, but not just any shape is a square or rectangle.

Inheritance theoretically provides a way to avoid reinventing the wheel by letting classes build off of a previous method. The individual changes can be done on a lower level to change a process. For instance, when calculating the area of a rectangle, you want to return height * width, while a square is technically the same, it can also be thought of as side2.

What It’s Not Good For, And What It Is

Inheritance makes a lot of sense when you have a good reason to use it, but it can be abused. For our instance of a rectangle and a square, when does it make more sense to use a square when dealing with arbitrary rectangles? If you’re working with only squares, when does it make sense to inherit from a rectangle? There are very few compelling cases when working with inheritance outside of inheriting someone else’s code, or when dealing with very specific overlapping code.

Inheritance is good for functions dealing with similar, but different data sources or overlapping data sets. If a manager is also an employee, when will what affects an employee not affect a manager? While you can theoretically just clone an employee into a manager and add what you need, encapsulate an employee into a manager class, it makes more sense to inherit these functions.

I would personally encapsulate the employee class into the manager class, but this habit comes from how I worked in Perl. Lua leaves you to your own devices so use whatever makes the most sense to accomplish implementing objects in Lua. Neither is going to be more right than the other.

Basic Inheritance

Here’s a basic inherited class:

--rectangle definition
local rectangle = {}
rectangle.__index = rectangle

function rectangle.new(side1, side2)
	local self = setmetatable({}, rectangle)
	-- internal processing for our class instantiation
	self.side1 = side1
	self.side2 = side2
	-- end of internal processing
	return self
end

function rectangle:getarea()
	return self.side1 * self.side2
end

--square definition
local square = {}
square.__index = square

function square.new(side)
	local self = setmetatable({}, square)
	self.side = side
	self.side1 = side --we set these for our rectangle class
	self.side2 = side --we set these for our rectangle class
	return self
end

setmetatable(square, {__index = rectangle})

--main:
local s = square.new( 10 )

print( s:getarea() )
print( s.side )

This gets us:

./class2-0.lua 
100
10

All we do is make our base object, which in this case is a rectangle, and then create a similar square class. Then we use the magic statement setmetatable(square, {__index = rectangle}) in order to build our inheritance. Now, we can use our square like a square or a rectangle.

Adding Functions

Just being able to use the functions from the inherited class is relatively useless. Let’s add a couple functions to our classes and flesh them out a bit:

#!/usr/bin/lua5.1

--rectangle definition
local rectangle = {}
rectangle.__index = rectangle

function rectangle.new(side1, side2)
	local self = setmetatable({}, rectangle)
	-- internal processing for our class instantiation
	self.side1 = side1
	self.side2 = side2
	-- end of internal processing
	return self
end

function rectangle:getside1()
	return self.side1
end

function rectangle:getside2()
	return self.side2
end

function rectangle:getarea()
	return self.side1 * self.side2
end

--square definition
local square = {}
square.__index = square

function square.new(side)
	local self = setmetatable({}, square)
	self.side = side
	self.side1 = side --we set these for our rectangle class
	self.side2 = side --we set these for our rectangle class
	return self
end

setmetatable(square, {__index = rectangle})

function square:getside()
	return self.side
end

--main:
local s = square.new( 10 )

print( s:getarea() )
print( s:getside() )
print( s:getside1() )

This gets us:

./class2-1.lua 
100
10
10

We can get our side1 or our side2 from our rectangle, but we can also get our side from our square. We can get anything from the rectangle in the square with how we implement this. The derived class can be extended as necessary with new functions. In addition to this, you can also replace functions in the derived class.

Replacing Functions

You can get the area from a rectangle by multiple the height by the width. You can do the same with a square, but you can also accomplish it by getting the side squared. Here’s an example of a previous function replaced by a new class function:

#!/usr/bin/lua5.1

--rectangle definition
local rectangle = {}
rectangle.__index = rectangle

function rectangle.new(side1, side2)
	local self = setmetatable({}, rectangle)
	-- internal processing for our class instantiation
	self.side1 = side1
	self.side2 = side2
	-- end of internal processing
	return self
end

function rectangle:getside1()
	return self.side1
end

function rectangle:getside2()
	return self.side2
end

function rectangle:getarea()
	return self.side1 * self.side2
end

--square definition
local square = {}
square.__index = square

function square.new(side)
	local self = setmetatable({}, square)
	self.side = side
	self.side1 = side --we set these for our rectangle class
	self.side2 = side --we set these for our rectangle class
	return self
end

setmetatable(square, {__index = rectangle})

function square:getside()
	return self.side
end

function square:getside1()
	return "s1: " .. self.side1
end

--main:
local s = square.new( 10 )

print( s:getarea() )
print( s:getside() )
print( s:getside1() )

This gets us:

./class2-2.lua 
100
10
s1: 10

We replace our getside1 from our rectangle with a new version in our square. The example we gave is functionally useless, but highlights the concept. The function indexed in our inherited class will take precedence over our parent class.

Exercises

Inheritance makes sense in certain situations. Think of a bank account, how would you implement inheritance and does it make sense to do so? What are some other situations where you could use inheritance and how do you ensure they make sense?