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!
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.
Here’s what we are going to accomplish today, a simpler way of defining a class and creating instances. It goes like this:
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
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
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.
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.
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.
class()
has to be callable like a function, because we write Siding(3)
to create a new Siding of length three.__index
value so that the methods of Siding will be available to every instance.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.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.
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
Why do we need a separate metatable rather than just using the class itself? Two reasons, at least:
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
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.
In that function, we create a new table to be our instance: every class instance is a new table, as always.
The metatable for the new instance is the class itself, as always.
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:
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.
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