JanetRossini.github.io

Lua, LSL, Blender, Python in Second Life


Project maintained by JanetRossini

A Stronger Way of Defining Classes

May 10, 2025 • [luametatablesobjects]


Yesterday, I made a repeated mistake using my current way of creating classes. We need a better way. Let’s have one! WARNING: Parts of this article go deep into metatable land!

Motivation

Up until now, we’ve created new classes using the simplest example I could find on the Internet, where you create a table to represent the class and set up a new method in it for creating instances. It looks like this:

local Siding = {}
function Siding:new(length)
   local instance = {}
   self.__index = self
   setmetatable(instance, self)
   instance.length = length
   instance.cars = {}
   return instance
end

Every time I set up a class, I have to decide what the creation parameters are and build a little structure like the above. That’s a little tedious, and since I’m almost guaranteed to copy and paste from one to create the next, I have at least once left the wrong creation parameters in place for a while.

Another issue is that I am used to creating class instances in Python and other languages, where the weird boilerplate is automatic and you just create an init method to set up the member variables of the instance. The result of that was that I forgot to use new three or four times out of four or five opportunities yesterday.

The Plan

Here’s what we are going to accomplish today, a simpler way of defining a class and creating instances. It goes like this:

Defining a Class

We will create a new function class that defines classes. That function will deal with all the metatable stuff behind the curtains.

We will start each class similarly, calling class and defining its init method:

local Thing = class()
function Thing:init(x, y, z)
   self.x = x
   self.y  = y
   self.z = z
end

Adding Methods

As before, to define a new method on our class, we can say something like this:

function Thing:adjust(factor)
   -- do whatever adjust does, using factor
end

Creating an instance

This is a bit different: we’re eliminating the new that we used to need. To create an instance of Thing, we just say something like:

local instance = Thing(15, 35, 42)

In the call to Thing, we provide the init parameters for the instance, x, y, and z in our case. Our init method will be automatically called, providing those parameters.

Day in and day out, that’s all we need to know.

How It Works

The following is pretty deep into metatables and embedded functions. It is safe for you to skip out right now and just use the thing. There is a copy of the class function at the end of this article, and I’ll try to set up a download of it if I can figure out Jekyll.

The better way (a review)

There is a better way of defining classes in Lua, and I happen to know it. More accurately, I happen to have a copy of it, and I plan to use it from here on. It’s a bit complicated, and it provides some capabilities that we may not need, so I might pare it down a bit and then put back the other bits if they’re actually needed. That would keep the function smaller, which would be good, and more clear to me, which would be excellent.

Let’s look again at what a class definition will look like with the new scheme. The class above will be defined like this:

local Siding = class()
function Siding:init(length)
   self.length = length
   self.cars = {}
end

To create an instance, instead of the current way:

local s1 = Siding:new(3)

We will write:

local s1 = Siding(3)

Again, just a bit nicer.

Overall, it seems to me that this scheme would be better, and I hope it seems so to you. If the above is to work, I think we know roughly what has to happen, based on our existing scheme of creating classes.

  1. Whatever comes back from class() has to be callable like a function, because we write Siding(3) to create a new Siding of length three.
  2. Our instances need to be set up with a suitable metatable and __index value so that the methods of Siding will be available to every instance.
  3. If the init is going to happen, the call to Siding(3) must call it. As a nicety, if there is no init, we will not try to call it.

How it works (trimmed)

Here is a trimmed-down version of the new class function, to make it a bit easier to explain. I’ll show the skipped parts later, if we need them. The comments are not mine, they came with the function when I found it. Let’s go bit by bit.

function class(base)
   local the_class = {}    -- our new class
   -- to be defined
   return the_class

We’re defining a function class that returns a table to represent the class. Our existing class definition is also a table. Almost everything in Lua is a table. Our existing class definition uses the class table as the metatable for instances, and looks up methods in that table.

The class has its own metatable

For a reason we’ll see in a moment, our class uses a separate metatable, which we create and apply to the class before we return it. With that added in, class looks like this:

function class(base)
   local the_class = {} 
   the_class.__index = the_class
   local mt = {}
   setmetatable(the_class, mt)
   return the_class
end

Calling a table(!)

Why do we need a separate metatable rather than just using the class itself? Two reasons, at least:

  1. Using the class as its own metatable is, for me, even harder to understand than regular metatables.
  2. More important: we want to call our class to create instances, like Car("name").

Right! For our scheme to work, we have to be able to call this table like a function: Siding(3) instead of Siding:new(3). This next bit is new to us, so let’s be careful and try to understand.

We can make any table callable if that table’s metatable includes a function named __call. Given a table my_tab, if we say my_tab(666), Lua knows you can’t call a table. So it looks in my_tab’s metatable for __call, and if it finds __call, it calls that function, passing my_tab as the first argument, followed by all the arguments we actually provided.

So here, when our user says MyClass(a, b,c), we need a function in __call that accepts the call, creates a new instance, and calls init with new_instance, a, b,c. And that’s done like this:

function class(base)
   local the_class = {} 
   the_class.__index = the_class
   local mt = {}
   -- the new part: make the class callable
   mt.__call = function(class_tbl, ...)
       local new_instance = {}
       setmetatable(new_instance,the_class)
       if class_tbl.init then
           class_tbl.init(new_instance,...)
       end
       return new_instance
   end

   setmetatable(the_class, mt)
   return the_class
end
  1. When we call the class, Lua runs the function mt.__call, providing the object called (our class) and the parameters sent on the call. So when we say Car("name"), this function runs.

  2. In that function, we create a new table to be our instance: every class instance is a new table, as always.

  3. The metatable for the new instance is the class itself, as always.

  4. If there is an init function in the class, we call it, passing the new instance and whatever parameters were provided on the class call. If we said Car("Fred"), we call init with self=new_instance and id="Fred".

We’ll skip the rest of the class function for now. This is more than enough, I’m sure. Let’s summarize what happens:

Review: Defining and Using a Class

We have a function class that defines classes. We start each class similarly, calling class and defining init:

local Thing = class()
function Thing:init(x, y, z)
   self.x = x
   self.y  = y
   self.z = z
end

To define a new method on our class, we can say something like this:

function Thing:adjust(factor)
   -- do whatever adjust does, using factor
end

To create an instance of Thing, we just say something like:

local instance = Thing(15, 35, 42)

The class’s init is automatically called with those parameters.

Day in and day out, that’s all we need to know.

The Whole Thing

Here, without further discussion, is the complete class function as I’m using it today. We’ll see it in use in the next article.


function class(base)
   local the_class = {}    -- our new class
   if type(base) == 'table' then
       -- our new class is a shallow copy of the base class!
       for i,v in pairs(base) do
           the_class[i] = v
       end
       the_class._base = base
   end

   -- the class will be the metatable for all its objects,
   -- and they will look up their methods in it.
   the_class.__index = the_class

   -- expose a constructor which can be called by <classname>( <args> )
   local mt = {}
   mt.__call = function(class_tbl, ...)
       local new_instance = {}
       setmetatable(new_instance,the_class)
       if class_tbl.init then
           class_tbl.init(new_instance,...)
       else
           -- make sure that any stuff from the base class is initialized!
           if base and base.init then
               base.init(new_instance, ...)
           end
       end

       return new_instance
   end

   the_class.is_a = function(self, klass)
       local m = getmetatable(self)
       while m do
           if m == klass then return true end
           m = m._base
       end
       return false
   end

   setmetatable(the_class, mt)
   return the_class
end