Skip to content

Instantly share code, notes, and snippets.

@liquidev
Last active April 30, 2024 16:24
Show Gist options
  • Save liquidev/3f37f94efdacd14a654a4bdc37c8008f to your computer and use it in GitHub Desktop.
Save liquidev/3f37f94efdacd14a654a4bdc37c8008f to your computer and use it in GitHub Desktop.
My attempt at explaining how to implement classes in Lua

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.

Metatables

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"].

__index

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

Method call syntax

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.

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

Inheritance

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

Generalizing

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)

Wrapping up

If you want an example of a more comprehensive, yet still minimalistic, and fast class library, check out rxi's classic.

@ThatsNasu
Copy link

omg, THATS it? i feel like an absolute idiot now xD thanks for cleaing this up c:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment