Tiny OOP in Lua
Recently I was participating in a conversation about popular programming languages and I brought up Lua. I was met with a response like this:
Lua? Can you even do anything with this language?
The conversation then shifted towards discussing modern dynamic languages, mostly Python, and how it gives you everything Java does without being as complicated as Java.
I’m not a Python fan - in fact, I think Python is a badly designed language and should be avoided. But people tend to like it for some reason. I also think that Lua is a much better language and it’s worth considering it before jumping to Python or alternatives.
I do agree that Lua is much smaller than Python, and it can do less by itself, as there are no batteries included in the language. But it also means that you can bring in only what you need for the project.
And Lua is extremely flexible too. When the conversation shifted towards discussing Python, one particular thing was discussed the most - Python is object-oriented. Lua isn’t, as there are no classes, no inheritance, or such. I brought up Lua again in the conversation, saying that it’s quite easy to add object orientation to Lua if you need it for your project. People were skeptical so I decided to write this post.
Most of the code here I wrote on my phone while guys were arguing about Python, so I wasn’t participating in the conversation much. After I was done, I showed them the following example, but the only response I got was a yet again skeptical “neat”.
Neat.
So here’s a basic OOP-style code in Lua.
First, we need a class:
local Shape = {}
It’s just a table, but now let’s add a new
method to it and do some metatable trickery:
function Shape:new(obj)
self.__index = self
return setmetatable(obj or {}, self)
end
Now we can create an object like this:
local s = Shape:new{}
It’s still a table, but it has Shape
as it’s metatable.
It means that we can add methods directly to a class, and all objects would share them:
function Shape:setOrigin(x, y)
self.x, self.y = x, y
end
function Shape:getOrigin()
return self.x, self.y
end
We can set the shape’s origin, and then obtain it with these methods:
s:setOrigin(1, 1)
print(s:getOrigin()) -- 1, 1
Note on
self
: in Luaself
is an ordinary variable name, that is implicitly declared when we use method declaration syntax. In other words, these two function definitions are the same thing:function Shape:getOrigin() -- the colon after Shape return self.x, self.y end function Shape.getOrigin(self) -- the dot after Shape return self.x, self.y end
It’s just a bit more convenient to write
Class:method(...)
thanClass.method(self, ...)
when defining a function. And when we’re calling a method with:
instead of.
it automatically passes the object as the first argument. So again, these are the same:s:setOrigin(1, 1) s.setOrigin(s, 1, 1)
But what makes objects special is the ability to build upon objects using inheritance.
So let’s create a new class that inherits all of the methods of Shape
and provides some new on its own:
local Rectangle = Shape:new{}
function Rectangle:setWidth(width)
self.width = width
end
function Rectangle:setHeight(height)
self.height = height
end
function Rectangle:area()
return self.width * self.height
end
We’ve defined a new class Rectangle
that inherits all of the Shape
methods:
local rect = Rectangle:new{width = 2, height = 4}
rect:setOrigin(3, 4)
print(rect:getOrigin()) -- 3, 4
print(rect:area()) -- 8
Here’s how this works:
Rectangle
inheritsnew
fromShape
like any other method. This time, however, whennew
executes, theself
parameter will refer toRectangle
. Therefore, the metatable ofrect
will beRectangle
, whose value at__index
is alsoRectangle
. So,rect
inherits fromRectangle
, which inherits fromShape
.
We can go even further, defining a class that would inherit from Rectangle
but first, let’s add two helper functions:
local function implements(obj, class)
local mt = getmetatable(obj)
if nil ~= mt then
if class == mt.__index then
return true
else
return implements(mt.__index, class)
end end
return false
end
local function super (class)
return (getmetatable(class) or {}).__index
end
The first function implements
is a predicate that checks if a given object implements a given class:
print(implements(rect, Shape)) -- true
print(implements(rect, Rectangle)) -- true
The second function super
allows us to access a superclass of a class.
For example, to call a superclass method, like a constructor.
We can create a new class that inherits from Rectangle
and use it to alter the inherited constructor:
local Square = Rectangle:new{}
function Square:new(obj)
self.__index = self
local obj = super(self):new{width = obj.width, height = obj.width}
return setmetatable(obj, self)
end
You can see that Square
implements all of the methods:
local square = Square:new{width = 10}
print(implements(square, Shape)) -- true
print(implements(square, Rectangle)) -- true
print(implements(square, Square)) -- true
square:setOrigin(5, 6)
print(square:getOrigin()) -- 5, 6
print(square:area()) -- 100
This isn’t a new kind of trick though. All of this was greatly described in the Programming In Lua book. In fact, I straight up took the explanation from this page and adjusted class names in it to represent the code on this page.
Now, of course, this leaves out a lot of stuff like encapsulation, and multiple inheritance, but you can build on this simple concept further if you really need to. Moreover, there are multiple ways of achieving object orientation in Lua. You can define the object’s methods in five different ways. But in my practice even what I did here is way more than I ever needed.
What I wanted to show here is the flexibility that Lua provides you with. If you need something that’s missing in pure Lua, the same pure Lua probably has a way to add that to the language. Over the years I’ve created lazy sequences, immutable data structures, and asynchronous channels all in pure Lua. With C modules you can add even crazier stuff.
Lua is a very capable language. The community made a lot of libraries. If you want so-called “batteries” there’s Penlight. If you’re making games, you have LÖVE2D that gives you its own batteries and a plethora of small Lua libraries for game-making are available too.
If you’re not a fan of the syntax or want more features in the base language you can choose amongst other languages compile to Lua. For example, there’s MoonScript which is whitespace-based like Python. Or there are Fennel and Urn if you’re into lisps. And probably other languages.
Lua is fast, has a small footprint, is easily embeddable, and is very flexible. It’s a shame it’s not taken seriously most of the time.