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()
  • class Cat, extends Animal
    • function speak()

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 syntax Object(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.