Classes in Lua
While reading Lua, you may have stumbled upon something that looks like this:
-- Declare a base `Entity` class...
local Entity = Object:inherit()
function Entity:tick() end
function Entity:draw() end
-- and an inheriting `Player` class.
local Player = Entity:inherit()
This is the way prople generally approach object-oriented programming in the language.
For someone coming from a language like Java, where classes are a syncactic construct—class Cat extends Animal
—it can feel weird to see them handled this way—as local variables, using regular functions to implement inheritance.
But worry not! This tutorial will hopefully clear up any confusion you might have, using beginner-friendly language, and simple examples.
Metatables
Before we start, we need to talk about metatables. These are Lua’s way of allowing you to overload operators.
Consider an operation like +
:
print(1 + 2) --> 3
The +
operator, by default, performs arithmetic addition.
However, with metatables, we can overload its meaning for when it’s used with our own table on the left.
local v = { x = 1, y = 2 }
setmetatable(v, {
__add = function (t, u)
return t.x + t.y + u
end,
})
print(v + 3) --> 6
Overloadable operators in Lua include not only your usual arithmetic +
, -
, *
, /
, but also things like indexing tables a[b]
, creating new indices in tables a[b] = c
, or function calls a(b, c, d)
.
Each operator has a special name in the metatable, and each operator’s name is prefixed with __
, to signal that it’s special.
__index
Today, we’ll be focusing on __index
, because it’s arguably the most important of them all.
It allows us to specify what should be done when the a[b]
indexing operator fails (is about to return nil
.)
Consider this example.
local t = { a = 1 }
print(t.b) --> nil
In this case, t
does not have a key "b"
, and t
has no metatable with __index
, so nil
is returned.
So let’s try adding that __index
function, to tell Lua what to do instead.
local fallback = { b = 2 }
setmetatable(t, {
-- The first argument is the table that's indexed,
-- and the second argument is the index.
-- i.e. the arguments map to `the_table[index]`.
__index = function (the_table, index)
return fallback[index]
end,
})
print(t.b) --> 2
Our function is called, it looks in fallback
to figure out what to return instead, and indeed—2
is returned instead of nil
!
However, __index
is special—it does not have to be set to a function.
We can also set it to a table, as a shorthand for the above form.
setmetatable(t, {
__index = fallback,
})
print(t.b) --> 2
This way of doing things avoids a lot of typing, as well as an extra memory allocation coming from that local function—which can get costly if you run it many times in a game loop!
Method call syntax
There is one thing we need to get out of the way before we move on, and that is Lua’s method call syntax a:method(b)
.
This syntax is equivalent to the following.
a.method(a, b)
Basically, the thing before the colon :
is passed as the first argument to the thing before :
’s method
function.
Lua also has a syntax sugar for declaring functions on tables:
local t = {}
function t.do_stuff()
print("hi")
end
-- equivalent to:
t.do_stuff = function ()
print("hi")
end
So to complement the :
method call syntax, there’s also the :
function declaration syntax, which inserts a self
parameter before all the other ones.
function t:do_thing()
self.aaa = 1
end
-- equivalent to:
function t.do_thing(self)
self.aaa = 1
end
The call and declaration syntaxes are not tied together in any way, so you can call :
-defined functions with .
and vice versa, but it’s probably better not to.
Bear in mind that your function definitions also serve the purpose of documentation, and using the :
syntax in definitions suggests that the way your function is supposed to be called is through the :
operator.
With that knowledge, we can more on to modelling classes.
Classes
We can use the __index
fallback operator to model classes quite easily.
Let’s create a class Cat
, with two functions meow
and feed
.
local Cat = {}
function Cat:meow()
print("meow")
end
function Cat:feed()
self.food = self.food + 1
end
We will also need a function for creating cats, which we’ll name new
.
function Cat:new()
local cat = {}
cat.food = 10
return cat
end
We can now use the API like so:
local kitty = Cat:new()
Cat.meow(kitty)
Cat.feed(kitty)
print(kitty.food) --> 11
However, note how we have to namespace the Cat
functions explicitly, and we cannot use the :
method call operator yet.
The table returned by Cat:new()
does not have the functions meow
and feed
for that to work.
So to provide it with these functions, we can use our handy __index
metamethod.
function Cat:new()
local cat = {}
cat.food = 10
-- setmetatable returns its first argument. How convenient!
return setmetatable(cat, { __index = Cat })
end
Now, we’re able to create cats that can meow on their own.
kitty = Cat:new()
kitty:meow()
kitty:feed()
print(kitty.food) --> 11
However, creating an extra metatable every single time we create a cat is pretty inefficient!
We can exploit the fact that Lua doesn’t really care about metatable fields it doesn’t know about, and make Cat
itself into a metatable.
Cat.__index = Cat
function Cat:new()
local cat = {}
cat.food = 10
return setmetatable(cat, Cat)
end
But note how we’ve declared Cat:new
with the special method syntax.
We call the function like Cat:new()
, which is equivalent to Cat.new(Cat)
, which means that the implicit self
parameter is the Cat
table already!
Thus, we can simplify the call to setmetatable
, to remove the redundant reference to Cat
.
return setmetatable(cat, self)
With all these improvements, here’s how the code looks so far.
local Cat = {}
Cat.__index = Cat
function Cat:new()
local cat = {}
cat.food = 10
return setmetatable(cat, self)
end
function Cat:meow()
print("meow!")
end
function Cat:feed()
self.food = self.food + 1
end
Inheritance
Given this fairly simple way of creating classes, we can now expand this idea to inheritance.
Conceptually, inheriting froma class is pretty straightforward: what we want to do, is to have all of the parent class’s methods available on the child class.
I think you might see where this is going now: all we need to do to create a subclass, is to create a new class, whose metatable’s __index
points to the parent class.
Let’s rewrite our example with the kitty to generalise animals under a single class.
-
class
Animal
, abstract-
variable
food
: integer -
function
speak()
-
function
feed()
-
variable
-
class
Cat
, extendsAnimal
-
function
speak()
-
function
Starting with the base Animal
class…
local Animal = {}
Animal.__index = Animal
-- We don't create a `new` method, because we don't want people
-- creating "generic" animals. This makes our class _abstract_.
-- speak() is a function that must be overridden by all subclasses,
-- so we make it error by default when called.
function Animal:speak() error("not implemented") end
function Animal:feed()
self.food = self.food + 1
end
We can define Cat
to be a subclass of Animal
, and have it inherit Animal
’s keys, by using __index
.
local Cat = {}
-- We still need to override __index, so that the metatable
-- we set in our own constructor has our overridden `speak()` method.
Cat.__index = Cat
-- To be able to call `Animal` methods from `Cat`, we set it
-- as its metatable. Remember that `Animal.__index == Animal`.
setmetatable(Cat, Animal)
function Cat:new()
-- Ultra-shorthand way of initializing a class instance!
-- No need to declare any temporary locals, we can pass
-- the table into `setmetatable` right away, and it will
-- return back the table we passed to it.
return setmetatable({
food = 1,
}, self)
end
-- Don't forget to override speak(), otherwise calling it
-- will error out!
function Cat:speak()
print("meow")
end
Note now how declaring speak
does not modify Animal
.
For that, we would need to set the __newindex
metamethod on the Animal
, not just __index
.
Now we can create instances of Cat
, and it will inherit the feed
method from Animal
.
local kitty = Cat:new()
kitty:speak()
kitty:feed() -- inherited!
print(kitty.food) --> 2
Packing it up into a nice box
With all this, we are now ready to pack this subclassing functionality into a nicer package.
Speaking of package, let’s create a module class.lua
.
local Class = {}
Class.__index = Class
return Class
Now, let’s create a function for inheriting from the class.
-- insert above `return Class`
function Class:inherit()
local Subclass = {}
Subclass.__index = Subclass
-- Note how `self` in this instance is the parent class,
-- as we call the function like `SomeClass:inherit()`.
setmetatable(Subclass, self)
return subclass
end
This is going to let us cleanly inherit from classes, without needing to copy and paste all the __index
and setmetatable
boilerplate into all subclasses.
local Class = require "class"
local Sub = Class:inherit()
The other boilerplatey bit was initialisation, so let’s take care of that.
-- insert below the `end` of `function Class:inherit()`
-- By default, let's make the base `Class` impossible to instantiate.
-- This should catch bugs if a subclass forgets to override `initialize`.
function Class:initialize()
error("this class cannot be initialized")
end
-- `...` is Lua's notation for collecting a variable number of arguments
function Class:new(...)
local instance = {}
-- `self` is the class we're instantiating, as this function
-- is called like `MyClass:new()`
setmetatable(instance, self)
-- We pass the instance to the class's `initialize()` method,
-- along with all the arguments we received in `new()`.
self.initialize(instance, ...)
return instance
end
Having that, we can now rewrite our Animal
example to use our super simple class library.
local Class = require "class"
---
local Animal = Class:inherit()
-- We'll provide a convenience function for implementers,
-- for initialising the food value, as well as any other
-- base fields that may come up.
function Animal:_initialize()
self.food = 1
end
-- However, we do not want to override initialize(), as
-- that would make our class concrete rather than abstract!
-- Remember that we don't want to make it possible to create
-- Animal instances on their own.
function Animal:speak()
error("unimplemented")
end
function Animal:feed()
self.food = self.food + 1
end
---
local Cat = Animal:inherit()
-- Instead, we override initialize() in Cat.
function Cat:initialize()
self:_initialize()
end
function Cat:speak()
print("meow")
end
Having a class library like this makes things a lot more convenient, as we no longer have to mess with raw metatables!
All we need to do is call inherit()
and new()
, and the magic is done for us.
local kitty = Cat:new()
kitty:speak()
kitty:feed()
print(kitty.food)
Wrapping up
If you followed this tutorial from beginning to end, you now have a simple library for object-oriented programming in Lua, which supports creating classes and inheriting from them.
To further your understanding, you may want to think about the following:
-
How would you call the superclass’s implementation of a function overridden by the subclass? Can you think of ways to make it convenient and easy to remember?
-
Our class library implements a Ruby-style
Object:new(args)
function for constructing new instances of our class. Python, however, uses the syntaxObject(args)
for constructing instances of objects. Can you think of a way to make your class library use the Python-style syntax? -
Define a 2D vector class using our class library. Can you think of a way to make use of Lua’s native
+
,-
,*
,/
math operators, instead of named functions like:add()
,:sub()
,:mul()
,:div()
? -
Try implementing an
object:instanceof(Class)
function, which checks that an object instance inherits from a given class. -
Lua is a minimalistic, multi-paradigm language. Can you think of the benefits and drawbacks towards doing object-oriented programming in Lua?
- What are some problems for which this style of programming would lend itself as particularly good?
- and likewise, what are some areas in which this style might not work so well?
Further reading
You may wanna check these links out for additional reference.
-
The Lua documentation on metatables—there’s lots of other operators you can overload!
-
rxi’s
classic
module—it’s an example of a good, but small class library that has all the features you’d ever need.