If you're reading this document, you're probably one of many people confused about how to implement OOP-like classes in Lua. But no worries! This document aims to explain all the fundamental concepts in beginner-friendly language.
Before we start, we need to talk about metatables. These are Lua's way of allowing users to overload operators. Operators include
arithmetic +
, -
, *
, /
, etc., but also things like indexing tables a[b]
, creating new indices in tables a[b] = c
, function
calls, a(b, c, d)
, you get the idea.
We can set the metatable of a table using setmetatable(t, metatable)
. The metatable
is another table, that contains fields for overriding
these operators. The most important field we'll be focusing on is __index
, which defines a fallback for the a[b]
operator, so also
a.b
, which is sugar for a["b"]
.
The __index
field is used when an otherwise nil
field is accessed in a table. Consider this:
local t = { a = 1 }
print(t.b) --> nil
In this case, t
does not have a metatable with __index
, so nil
is returned. We can override __index
however; all operators can
be overridden with functions. In our case we'll create a function that reads a field from another table:
local fallback = { b = 2 }
setmetatable(t, {
-- The first argument is the table that's indexed, and the second argument is the index,
-- ie. the arguments map to `the_table[index]`.
__index = function (the_table, index)
return fallback[index]
end,
})
print(t.b) --> 2
However, there's a more compact and fast way of doing this; __index
is special, because we can not only set it to a function, but also
a table:
setmetatable(t, {
__index = fallback,
})
print(t.b) --> 2
There's one thing we need to get out of the way before we move on, and that is Lua's method all syntax a:method(b)
.
When used to call a function, this syntax is equivalent to:
a.method(a, b)
Basically, the thing before :
is passed as the first argument to the thing before :
's method
function.
Lua also has syntax sugar for declaring functions on tables:
local t = {}
function t.do_stuff()
print("hi")
end
To complement the :
call syntax, there's also the :
function declaration syntax.
function t:do_thing()
self.aaa = 1
end
-- desugars to
function t.do_thing(self)
self.aaa = 1
end
As shown on the above example, this syntax simply inserts a self
parameter before all other parameters.
The call and declaration syntaxes are not tied together in any way, so the dot and colon syntax could be mixed however one wants... but it's probably better not to.
Now, with that knowledge, we can move on to creating classes.
We can use __index
fallback tables to model classes quite easily. Let's create a class Cat
with two methods meow
and feed
:
local Cat = {}
function Cat:meow()
print("meow!")
end
function Cat:feed()
self.food = self.food + 1
end
We also need a method for creating cats, which I'll call new
:
function Cat:new()
local cat = {}
cat.food = 10
return cat
end
We can now use the API like this:
local kitty = Cat:new()
Cat.meow(kitty)
Cat.feed(kitty)
print(kitty.food)
But, we cannot use the :
method call operator yet; the table returned by Cat:new
does not have the methods meow
and feed
.
To provide it with these methods, 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'll be able to create cats that can meow on their own:
kitty = Cat:new()
kitty:meow()
kitty:feed()
print(kitty.food)
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 that 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 method like Cat:new()
, which desugars to Cat.new(Cat)
,
which means that the implicit self
parameter is already the Cat
table! Thus, we can simplify the call to setmetatable
:
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
Given this fairly simple way of creating classes, we can now expand this idea to inheritance. Inheriting from a class is pretty simple: 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 is the parent class. Let's rewrite our example with the kitty to generalize animals under a single class:
Animal
- food: integer
: speak()
: feed()
Cat : Animal
: speak()
Let's now implement the Animal
class, as showcased before.
local Animal = {}
Animal.__index = Animal
-- We don't create a `new` method, because we don't want people creating "generic" animals.
-- speak is a function that must be overridden by all subclasses, so we make it error by default.
function Animal:speak() error("not implemented") end
function Animal:feed()
self.food = self.food + 1
end
Now, let's create our Cat
class as a subclass of Animal
:
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.
setmetatable(Cat, Animal)
function Cat:new()
-- Ultra-shorthand way of initializing a class instance
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 that declaring speak
does not modify Animal
. For that, we would need to set the __newindex
metatable field on the Animal
,
and not just __index
.
Now, we can create instances of the Cat
, and it'll inherit the feed
method:
local kitty = Cat:new()
kitty:speak()
kitty:feed()
print(kitty.food) --> 2
With all this, we are now ready to pack this subclassing functionality into a nicer package. Speaking of packages, let's create a module
class.lua
:
local Class = {}
Class.__index = Class
return Class
Now, let's create a method for inheriting from the class.
-- insert above `return Class`
function Class:inherit()
local Subclass = {}
Subclass.__index = Subclass
-- Note how `self` in this case is the parent class, as we call the method like `SomeClass:inherit()`.
setmetatable(Subclass, self)
return Subclass
end
This is going to let us cleanly inherit from classes without all the boilerplate:
local Class = require "class"
local Sub = Class:inherit()
The other boilerplate-y bit was initialization, 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 method 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 initializing the food value.
function Animal:_initialize_food()
self.food = 1
end
function Animal:speak()
error("unimplemented")
end
function Animal:feed()
self.food = self.food + 1
end
---
local Cat = Animal:inherit()
-- Don't forget that our initialize() method errors by default, so it has to be overridden.
function Cat:initialize()
-- We'll call the food initialization helper from the parent Animal class.
self:_initialize_food()
end
function Cat:speak()
print("meow")
end
Having a nice class library like this makes things a lot more convenient. No longer do we have to mess with raw metatables; all we need to
do is call inherit()
or new()
and the magic is done for us.
local kitty = Cat:new()
kitty:speak()
kitty:feed()
print(kitty.food)
If you want an example of a more comprehensive, yet still minimalistic, and fast class library, check out rxi's classic.
Thanks a lot, this cleared up a lot of confusion for me.