Deeper understanding of metatables

I’ve been using meta tables for a bit, but I really don’t TRULY understand how they work. What I’m mainly stumped on is the metamethods and setmetatable. I’ve been usually just copy pasting what I’ve seen before and setting up classes in the exact same way. But now, the way I used to do it seems to be getting in the way of my frameworks.

For Example
Lets say I had a a table with an __index

local Index = {}

local Table = {}
Table.__index = Index

And I created a new function in the Index table.

local Index = {}

local Table = {}
Table.__index = Index

function Table.new()
           local self = setmetatable({}, Table)


          return self
end

function Index:Trigger()
       print("Triggered the Index")
end 

Yeah that works fine, right? But this is where my issue occurs.
Lets say theres an even LARGER table I want the index to be inside of, named “Framework”. Whenever I call on a method from self, I want self to check through Index, and if it doesn’t find the method or object in Index, it searches through Framework finally. I assumed I’d just have to do this.

local Index = {}

local Table = {}
Table.__index = Index

function Table.new(_FrameworkObject)
           Index.__index = _FrameworkObject

           local self = setmetatable({}, Table)


          return self
end

function Index:Trigger()
       print("Triggered the Index")
       self:TriggerFrameworkMethod() -- attempt to call missing method "TriggerFrameworkMethod" of table.
end 

No? I’m a little lost on how things really go in the woodwork with meta tables.

P.S. I wrote this by hand on-site, so if there are any syntax errors it was NOT on purpose.

I’m having difficulty understanding what you’re asking, so I will explain bit by bit.

When you use setmetatable, you define special behavior to your table, that’s what a “metatable” is, a special behavioral list. There are many metamethods, such as __index, __newindex, and so on, and they define how your table reacts to certain requests.


" The use of a table as an __index metamethod provides a cheap and simple way of implementing single inheritance. A function, although more expensive, provides more flexibility: We can implement multiple inheritance, caching, and several other variations." - Lua Manual

When you use __index, and if the value in the table is nil, it will fall back to the index values of the __index table, this includes methods (functions) and other variables. Here is an amazing picture made by metatablecatmaid


Here is a simple example you can try:

local Reference = {a = 2, b = 3}
local Actual = setmetatable({a = 1}, {__index = Reference})
print(Actual.a, Actual.b) -- Prints "1 (Value inside Actual) 3 (Fallback to Reference)"

Here is an example, with “self” included. As you can see, when an array has a “:” method, you can utilize self, which will be equal to “Actual” in this case

local Reference = {}

function Reference:Test()
    print(self)
end

local Actual = setmetatable({a = 2}, {__index = Reference})
Actual:Test() -- Prints out {a = 2}
-- As you can see, we are allowed to run :Test() because it inherited the method from "Reference". It will simply use "self" as {a=2}
Reference:Test() -- This prints out the entirety of "Reference", because in this case, self is itself. 
-- You expect a print of something like {Test = "function"}

So essentially, when you do OOP like this:

local Car = {}
Car.__index = Car

function Car.new()
    return setmetatable({}, Car) -- Here we set "self" as {} but it could be anything
end

function Car:Boost()
end

return Car

What you’re essentially doing, is inheriting all the methods in the “Car” array, with the special privilege that you can use “self”, which means that all the methods are already predefined and will all do the same thing, only “self” changes in certain scenario


local Car1 = Car.new()
Car1:Boost() -- Will run the boost function, with self set as {}

This is essentially equivalent of doing something like this:

Car:Boost(Car1) -- Pass the reference directly, and instead of using "self" you use the array "Car1"
4 Likes

__index can accept both function and table.
If table has value set to nil it will look for said value in metatable.
Althrough you should avoid metatable OOP in general
And instead relying on static C like OOP and sometimes closures for better perfomance.
Metatables is a thing that you want to avoid at all cost becouse its just an abstraction for a sake of it.

2 Likes

I’ve heard a lot of people stating metatable OOP is unnecessary in general if you’re not going to use inheritance with it, I think this is what OP is trying to achieve.

Also, your post on strictly typed “OOP” is pretty interesting :smile:

2 Likes

I understand that .__index basically links a table to a table

(aka)
if the value isn’t in table one it searches through table 2 for it, but what I want is this.

Basically, if it isn’t in table one, it searches through table two, and if not in table two, it’ll go through to table twos index, the framework. That’s like the final search option. Like this.

local framework = {value = 1}

local ComponentTable = {}
ComponentTable.__index = framework

local ComponentMethods = {}
ComponentMethods.__index = ComponentTable

print(ComponentMethods.value) -- I'd assume it'd look in ComponentTable and see nothing, then proceed to search Component Table's index, Framework to find the value.

(Just a clarification) That is not how you set metatables, you need to specifically use the setmetatable function. You’re only defining a field

local framework = {value = 1}

local ComponentTable = setmetatable({}, {__index = framework}) 

local ComponentMethods = setmetatable({}, {__index = ComponentTable})

print(ComponentMethods.value) -- prints out 1
2 Likes
local Actions = {}

local ActionHandler = setmetatable({}, {__index = Actions})


function ActionHandler.new(Framework)
	Actions = setmetatable({}, {__index = Framework})
	local self = setmetatable({}, {__index = ActionHandler})
	
	return self
end

function Actions:Trigger(...)
	print(Actions)
end



return ActionHandler

This is exactly what I’m attempting to do.

When ActionHandler.new is called on, I set the Actions Index to Framework, is that how I do it?

I think what you’re trying to do is setmetatable(Actions, {__index = Framework }), but it’s getting a little bit abstract.

local Actions = {}

local ActionHandler = setmetatable({}, {__index = Actions})


function ActionHandler.new(Framework)
	setmetatable(Actions, {__index = Framework})
	local self = setmetatable({}, {__index = ActionHandler})
	
	return self
end

function Actions:Trigger(...)
	print(Actions)
end



return ActionHandler

Doing that will make it so “self” inherits “ActionHandler”, then “Actions” and finally “Framework” (in order), though I’m uncertain the use case for this I lack a bit of context.

2 Likes

sorry for my terrible explaining skills, thank you bro. This comes in handy SO MUCH

1 Like