metatables
2025-02-08 we can set the metatable of a table using
setmetatable(t, metatable)
.2025-02-08 __index
2025-02-08 in this case,
t
does not have a metatable with__index
, sonil
is returned. to change this behaviour, we override__index
by telling Lua a function to run whenever the key doesn’t exist.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
2025-02-08 however, there is a more compact and faster way of doing this.
__index
is special, because in addition to being able to set it to a function, we can also set it to a table:setmetatable(t, { __index = fallback, }) print(t.b) --> 2
this avoids the need to allocate a local function, which can be costly if you run it many times in a game loop!
2025-02-08
classes
2025-02-08 we can now use the API like this:
local kitty = Cat:new() Cat.meow(kitty) Cat.feed(kitty) print(kitty.food) --> 11
but, note how we have to namespace the
Cat
functions specifically, and we cannot use the:
method call operator yet. the table returned byCat:new()
does not have the methodsmeow
andfeed
for that to work.2025-02-08 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
2025-02-08 but note how we’ve declared
Cat:new
with the special method syntax. we call the method likeCat:new()
, which desugars toCat.new(Cat)
, which means that the implicitself
parameter is already theCat
table! thus, we can simplify the call tosetmetatable
, to remove the redundant reference toCat
:return setmetatable(cat, self)
2025-02-08
inheritance
2025-02-08 conceptually, 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’s
__index
points to the parent class.2025-02-08 so, 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. function Animal:speak() error("not implemented") end function Animal:feed() self.food = self.food + 1 end
2025-02-08 we can define a
Cat
class as a subclass ofAnimal
: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. return setmetatable({ food = 1, }, self) end -- Don't forget to override speak(), otherwise calling it will error out! function Cat:speak() print("meow") end
2025-02-08
generalising
2025-02-08 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 instance is the parent class, as we call the method like `SomeClass:inherit()`. setmetatable(Subclass, self) return Subclass end
2025-02-08 the other boilerplaty 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 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
2025-02-08 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 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() self:_initialize() end function Cat:speak() print("meow") end
2025-02-08
further reading
2025-02-08 rxi’s
classic
module—it’s an example of a good, but small class library that has all the features you’d ever need.2025-02-08