Learn Lua in an Hour

Tables as classes

Now let's talk about using tables as classes.

Lua doesn't directly contain classes, but it's easy to use tables in an object-oriented way. When I talk about classes in Lua, I just mean tables used so that they combine data and behavior in one place, and where inheritance is possible.

When you treat a table as a class instance, you want to make sure that method calls have access to the instance. In C++ you would use the this pointer; in Python you would use self. Lua also uses a parameter called self.

I'm using the word method even though Lua itself doesn't distinguish between functions and methods - Lua sees them all as functions. So when I say the word method what I mean is: a regular Lua function that meets two conditions.

Condition 1: It's a value in a table used as a class. And,

Condition 2: It expects its first input to be a self parameter when it's called.

This will all become more clear with examples.

Now is a good time to explain colon syntax for function definitions and function calls.

The main purpose of colon syntax is to make it easier to define and call methods on tables used as classes.

Here's how we can define a method using regular syntax:

> t = {mynum = 343}
> function t.f(self) print(self.mynum) end

We can call the method like this:

> t.f(t)
343

When you're defining a function in a table, using a colon instead of a period inserts a first input parameter called self.

> function t:g() print(self.mynum) end

So g and f are essentially the same.

When you call a function in a table, using a colon inserts the table as the first parameter value passed to the function. So this:

> t:g()
343

is the same as this:

> t.g(t)
343

That's everything about colon syntax.

Let's make some example classes.

I'll make two classes. The first is a class that can print out values from a sequence of numbers that increase by 2 with each step. After that, we'll make a subclass based on the sequence of square numbers.

First I'll type out a constructor, then I'll use it, and then I'll explain what each line does.

>  Sequence = {}
>  function Sequence:new()
>>   local new_seq = {last_num = 0}
>>   self.__index = self
>>   return setmetatable(new_seq, self)
>> end

We can use this to get a new Sequence instance like this:

> seq = Sequence:new()

The first constructor line creates a new table that will be the returned instance. The second line makes sure that Sequence itself has an __index key pointing to itself. The purpose of this is to make Sequence useful as a metatable where instances will find their methods. The last line sets Sequence as the metatable for the new instance and returns the new instance.

So now we have a constructor and an instance, but we can't actually do anything yet with the instance. Let's change that by defining a couple methods on the class.

The next method returns the next number in the sequence. It doesn't change the state of any variables, though.

>  function Sequence:next()
>>   return self.last_num + 2
>> end

The forward method does change the state of the sequence by moving forward a given number of steps. It calls the next method to find out which number is next as it takes each step.

>  function Sequence:forward(n)
>>   for i = 1, n do
>>     self.last_num = self:next()
>>     print(self.last_num)
>>   end
>> end

Now I can print out some values in the sequence like this:

> seq:forward(5)
2
4
6
8
10

Now for something interesting.

We defined the forward method in a nice general way, so that it doesn't actually know anything about the sequence. It just calls the next method, which encapsulates the order of the sequence.

So we can make a subclass that prints out a different sequence by overriding the next method. Let's do that for square numbers.

>  Squares = Sequence:new()
>  function Squares:next()
>>   local root = math.sqrt(self.last_num) + 1
>>   return root * root
>> end

Squares is a subclass, so it may be surprising to see that it begins life as an instance of Sequence. This is actually a nice way to implement inheritance in Lua. It works because classes and instances are all just tables, and instance behavior is based entirely on metatables. In a minute I'll show a diagram that illustrates the relationship between the Square and Sequence tables.

Here is how we can use the Square subclass:

> sq = Squares:new()
> sq:forward(5)
1
4
9
16
25

Yay, it works.

Now let's take a look at a class diagram.

This focuses on the three tables sq, Squares, and Sequence.

We're thinking of Sequence and Squares as classes because they have access to a constructor and the forward and next methods.

When we look up a key on sq, Lua will use the first value it finds in the metatable chain. So calling sq:next will call the next method associated with Squares and not the one associated with Sequence, even though forward itself is defined in the Sequence table.

It's interesting to carefully consider a call to sq:forward, because somehow the forward method has to know to call the version of next in Squares and not the version in Sequence.

First, Lua finds the forward key defined in Sequence by looking up the metatable chain.

Then forward makes a call to self:next.

What is the value of self? It's set to sq. So when forward calls self:next, Lua again begins the lookup starting at the bottom of the metatable chain, with sq itself.

So any methods defined in Squares get to override methods defined in superclasses like Sequence.

This is how single inheritance works in Lua.

It's also possible to use multiple inheritance in Lua, which is where a class may have more than one immediate superclass. I won't cover this in detail, but I'll mention that the key idea is to use a function as a metatable's __index value instead of a table.

Next