Reputation: 657
How to mimic simple inheritance with parent and child class constructors in Lua?
I will provide and accept and answer and would greatly appreciate if you commented on it or edited interesting information into it.
Upvotes: 0
Views: 2364
Reputation: 657
This is a guide for mimicking basic parent/child class inheritance in Lua.
I assume that basic features and syntax of Lua are known to the reader. The following statements are especially important:
mt
of a table t
contains the __index
metamethod (thus mt.__index
), access to a non-existent key in t
is attempted to be resolved via what's assigned to mt.__index
.t
calls a function t.foo()
like this t:foo()
then t
itself is passed to foo()
as the first argument called self
. Inside the function, t
itself is accessible as self
.Base = {}
function Base:new(name)
Base.__index = Base
local obj = {}
setmetatable(obj, Base)
obj.name = name
return obj
end
function Base:sayName()
print(self.name..": My name is "..self.name..".")
end
Because of what the function Base:new(name)
does, Base
can now be seen as a new class, with Base:new(name)
being its constructor. Remember that Base
really is a table that sits somewhere in memory, not some abstract "class blue print". It contains a function Base:sayName()
. This is what we could call a method with respect to OOP lingo.
But how does Base:new(name)
let Base
act like a class?
Base.__index = Base
The Base
table gets an __index
metamethod, namely itself. Whenever Base
is used as a metatable, searches to a non-existent index will be redirected to... Base
itself. (Wait for it.) The line could also be written as self.__index = self
, because Base
calls :new(name)
and thus self
is Base
therein. I prefer the first version, because it clearly shows what's happening. Also, Base.__index = Base
could go outside of Base:new(name)
, however, I prefer having all the "set up" happen inside one scope (the "constructor") for the sake of clarity.
local obj = {}
setmetatable(obj, Base)
obj
is created as a new empty table. It will become what we think of as an object of the "class" Base
. The Base
is now the metatable of obj
. Since Base
has an __index
, access to non-existent keys in obj
well be redirected to what is assigned to Base.__index
. And since Base.__index
is Base
itself, access to non-existent keys in obj
will be redirected to Base
(where it would find Base:sayName()
, for instance)!
obj.name = name
return obj
The obj
(!) gets a new entry, a member, to which the constructor parameter is assigned. The obj
is then returned and is what we would interpret as an object of class Base
.
b = Base:new("Mr. Base")
b:sayName()
This prints "Mr. Base: My name is Mr. Base." as expected. b
finds sayName()
via the metatable-__index
mechanism as described above, because it doesn't have such a key. sayName()
lives inside Base
(the "class table") and name
inside b
(the "object table").
Child = {}
function Child:new(name, age) -- our child class takes a second argument
Child.__index = Child
setmetatable(Child, {__index = Base}) -- this is different!
local obj = Base:new(name, age) -- this is different!
setmetatable(obj, Child)
obj.age = age
return obj
end
function Child:sayAge()
print(self.name..": I am "..tonumber(self.age).." years old.")
end
The code is almost exactly the same as for the base class! Adding a second parameter in the Child:new(name, age)
(i.e. the constructor) is not especially noteworthy. Base
could also have had more than one parameter. However, the second and third line inside Child:new(name, age)
were added and that is what causes Child
to "inherit" from Base.
Note that Base
may contain Base.__index
, which makes it useful when used as a metatable, but that it has no metatable itself.
setmetatable(Child, {__index = Base})
In this line, we assign a metatable to the Child
class table. This metatable contains an __index
metamethod, which is set to the Base
class table. Thus, the Child
class table will try to resolve access to non-existent keys via the Base
class table. Thus, the Child
class table has access to all of its own and all of Base
's methods!
local obj = Base:new(name, age)
setmetatable(obj, Child)
In the first line, a new Base
object table is created. At this point, its __index
metamethod points to the Base
class table. However, right in line two, its metatable is assigned to the Child
class table. The reason why obj
does not lose access to Base
class methods lies in the fact that we redirected non-successful key accesses in Child
to Base
(by giving Child
the proper metatable) just before! Since obj
was created as a Base
object table, it contains all of its members. Additionally, it also contains the members of a Child
object table (once they are added in the Child:new(name, age)
"constructor". It finds the methods of the Child
class table via its own metamethod. And it finds the methods in the Base
class table via the metamethod in the Child
class table.
Note: With "Base
object table" I mean the table that is returned by Base:new(name)
. With "Base
class table" I mean the actual Base
table. Remember, there are no classes/objects in Lua! The Base
class table and the Base
object table together mimic what we think of as OOP behavior. The same goes for Child
, of course.
Also, scoping the assignment of Child
's metatable inside Child:new(name, age)
allows us to call Base
's "constructor" and pass the name
argument to it!
c = Child:new("Mrs. Child", 42)
c:sayName()
c:sayAge()
This prints "Mrs. Child: My name is Mrs. Child." and "Mrs. Child: I am 42 years old." as expected.
The sections above described how to implement OOP behavior in Lua. It is important to understand that
Base
class tableBase
object table returned by Base:new()
Child
class tableChild
object table returned by Child:new()
Referencing the correct tables is accomplished by the table's metatables.
__index
key. When used as metatables, they refer to themselves (i.e. where the class methods live). There is only one class table per "class".{__index = Base}
(which redirects calls to the Base
class table, i.e. where the Base
class methods live).__index
metamethod, calls to the object tables can be redirected to the class tables where the class methods live. If it is a child class table (which means it also has a metatable), the redirection can happen even further. There can be arbitrarily many object tables.Use the image above to follow along: What happens if a Child
object table c
tries to access a Base
method, e.g. c:sayName()
? Well: Has c
a sayName()
key? No. Does it have a metatable? Yes: Child
(the Child
class table). Does Child
have an __index
metamethod? Yes. Where does it point? To Child
itself. Does Child
have a sayName()
key? No. Does Child
have a metatable? Yes. Does it have an __index
metamethod? Yes. Where does it point? To the Base
class table. Does it have a sayName()
key? Yes! :-)
I am no Lua expert! I have only done some scripting in Lua so far, but over the last days I tried to wrap my mind around this. I found a lot of different, sometimes confusing solutions and finally arrived at this, which I would call the most simple but transparent solution. If you find any errors or caveats do not hesitate to comment!
Upvotes: 6