Okay yeah I probably should have explained what each setmetatable/__index pointer does here.
First off, the __index metamethod is invoked whenever you try indexing a table for an index (entry) that doesn’t exist. So for example if you try doing t.entry
, or t['entry']
, or t:entry()
, those are all examples of what indexing a table looks like, and what may invoke the __index metamethod.
This functionality of allowing tables to point to other tables in the case of nonexistent members is basically what gives you the ability for one object (table) to inherit things from another table.
So for example, if you do:
local t = {}
local othert = {
someProp = true;
}
othert.__index = othert -- can really be any table you want, doesn't have to be 'othert'
print(t.someProp) -- nil
setmetatable(t, othert)
print(t.someProp) -- true. because when we index t, and find no entry, we then invoke t's metatable's __index metamethod which points back to othert
-- since the entry exists in othert, it will return true instead
So basically the process is as follows when you try to index a table for a nonexistent entry:
1 - check t
for an entry ‘someProp’
2 - when t.someProp
doesn’t exist, get t
’s metatable (othert
), and looks for an __index metamethod
3 - if __index is found in the metatable, it either calls the function (if __index is a function), or looks to the table __index points to for the entry
4 - if the entry is found in whatever __index points to, it returns that entry instead
In our animal module:
local animal = {}
animal.__index = animal
function animal.new()
local self = setmetatable({}, animal)
self.Traits = traits.new()
return self
end
Suppose we call animal.new(). We now have a new animal object.
What happens when we index it for Traits
(do animal.Traits
)? It will just return the Traits
entry in the animal object.
However, what happens when we index it for GetTraits
?
1 - We look in our animal object and see that no entry GetTraits
exists.
2 - We get the animal’s metatable, go through the __index metamethod (which points back at the animal
module itself) then look in the metatable for a GetTraits
member (which is our GetTraits method), then returns that function object.
That’s the premise of “first-level” metatables (just a table with a metatable).
The same premise applies to any number of chained tables.
So this is our tiger module:
local animal = require(script.Parent:WaitForChild('Animal'))
local tiger = setmetatable({}, animal) -- this will inherit all methods in `animal` module
tiger.__index = tiger
function tiger.new()
local self = setmetatable(animal.new(), tiger)
self.Striped = true
return self
end
First thing, we call tiger.new()
to create a new tiger object.
The constructor itself will create a new object of base animal
class, then overwrites its metatable to be tiger. This allows inheritance of properties.
What happens when we index tiger for Striped
? Of course, it exists, so it will return true as we expect.
What happens when we index tiger for Traits? Again since tiger inherits animal
, all properties from animal
will be there. No invocation of metamethods yet.
However, what happens when we index tiger for Attack
?
1 - Look in our tiger object, we see tiger.Attack is nil
2 - Get our tiger object’s metatable (tiger
module itself) for an __index metamethod which points back at the tiger
module.
3 - Look in __index (tiger
module) for a member “Attack”
4 - It exists, so we return that function object.
Now, where the part of chained metamethods comes into play. What happens if we try indexing tiger
for a member GetTraits
?
1 - Look in our tiger object, we see tiger.GetTraits is nil
2 - Get our tiger object’s metatable (tiger
module itself) for an __index metamethod which points back at the tiger
module.
3 - Look in __index (tiger
module) for a member “GetTraits”. It doesn’t exist.
4 - Since step 3 failed, we try to get the metatable of the tiger
module itself.
5 - Since tiger
module has a metatable, we look in its metatable for an __index metamethod. Instead of pointing back at the tiger
module, it points to the animal
module
6 - It looks in the animal
module for an entry Attack
7 - animal.Attack
exists, so we return that function object.
You basically repeat steps 5-7 until you land on a table that doesn’t have a metatable which is how a table can have a metatable that has a metatable that has a metatable and so on indefinitely.