LCsa
LCsa

Reputation: 657

How to mimic simple inheritance with base and child class constructors in Lua (Tutorial)

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

Answers (1)

LCsa
LCsa

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:

  • There are no classes or objects in Lua, just tables.
  • A table can have a metatable.
  • If the metatable 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.
  • If 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 class

Code

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?

Explanation

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.

Demonstration

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 class

Code

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.

Explanation

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!

Demonstration

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.

Conclusion

The sections above described how to implement OOP behavior in Lua. It is important to understand that

  • Base methods live inside the Base class table
  • Base members live inside the Base object table returned by Base:new()
  • Child methods live inside the Child class table
  • Child members live inside the Child object table returned by Child:new()

Referencing the correct tables is accomplished by the table's metatables.

  • All class tables assign themselves to their __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".
  • Base classes don't have a metatable. They are used as metatables.
  • Child class tables also have a metatable, namely {__index = Base} (which redirects calls to the Base class table, i.e. where the Base class methods live).
  • All object tables assign their corresponding class tables as metatables. Since the class tables have set their __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.

Schematic table overview

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! :-)

Note

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

Related Questions