I've since moved the tutorial over to my website. Clarified some of the wording, added exercises at the end, added links to useful resources.
For historical reasons, I'm keeping the original text here. But if you've just stumbled upon this tutorial for the first time, you may want to check out the revised version!
https://riki.house/programming/lua/classes
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) --> nilIn 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) --> 2However, 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) --> 2There'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")
endTo 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
endAs 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
endWe also need a method for creating cats, which I'll call new:
function Cat:new()
local cat = {}
cat.food = 10
return cat
endWe 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 })
endNow 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)
endBut 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
endGiven 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
endNow, 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")
endNote 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) --> 2With 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 ClassNow, 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
endThis 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
endHaving 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")
endHaving 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.