They let you override default behavior that tables normally have. Or add some behavior that’s not there by default. I’m going to focus on the override part here.
Let’s look at this dictionary here
local dict = {
x = 5,
y = 2,
z = 5
}
Now say we want to look something up. We could of course just call dict[“x”] which would return us the value stored under the key X. What’s important to know though is what happens if the thing we try indexing doesn’t exist?
dict[“pancakes”] searches for it and immediately returns nil when it can’t find it right? The answer is kind of. It actually makes one more stop after it can’t find it. It checks the metatable to see if you’ve overriden the default behavior of just returning nil.
So let’s look here
local example = {}
example.__index = example --what?
That, doesn’t make a ton of sense at first glance right? But what you are doing is pretty much exactly what it says. You’re making it reference itself with a “.__index” key. So now if you look that up, you’ll get the table itself returned. At this stage it doesn’t make a whole lot of sense why we would do that though.
So let’s move onto the next part
function example.new()
local t = {}
setmetatable(t, example)
return t
end
Now we have set a metatable. So now t has a metatable. Which means now, when you try indexing t (t[“pancakes”]). If it finds t doesn’t have a key that matches it, it will then look at it’s metatable for instructions on what to do next. Specifically since you are indexing it, it’s going to look at the key __index. If the key is a function, it will run that and return whatever you return. So remember, “example” was the metatable it’s going to be looking at for more instructions. So if you were to change the second line to be
example.__index = function()
return true
end
Now t[“pancakes”] would find that there is no “pancakes” in t, then look at example
to see that it has a function to run instead of returning nil like it would by default. But if you recall, we set it as a reference to it’s own table. So this means that if it doesn’t exist in t, it’s going to look inside example next before it checks examples metatable for further instructions.
And that’s basically it. It let’s you override specific behaviours of tables. There is a list in the documentation of what you can override and some things to consider.
As for in practice, consider a design where you want to create lots of data. Say for example vector3s. Each vector 3 is going to have some functions you want to be able to use it with. Like :Lerp(). You could just put that inside the vector3 table of course, but that’s a waste of memory. It’s better to have a table of functions that get checked if you try to call something that doesn’t exist inside vector3 so that each vec3 can reference the same functions instead of storing its own copy.