Part 2! This is all about metatables, which will be the main tool to implement OOP in Lua. Metatables, put simply, are Lua tables that describe the behavior of other Lua tables. You can use getmetatable(tab) and setmetatable(tab, meta) to manage a table’s metatable:
local my_tab = {}
local my_metatable = {}
setmetatable(my_tab, my_metatable)
Now that you’ve set a table’s metatable, you can now set metamethods. These are special keys in the metatable that define specific behavior when set to a function, for example: __index , __add , __sub , __tostring , __eq and others. Notice how their names all being with two underscores (_). These metamethod functions are called when you do things with the table in question. It’s important to take time and read up on the behavior of each metamethod on your own, like how __add is called when you add two tables with the same metatable.
We’ll be using __index to implement classes and objects. This metamethod is called when you try to index a key (doing tab[key] or tab.key, remember these are equivalent) that hasn’t been set in a table yet:
local my_tab = {}
local my_metatable = {}
setmetatable(my_tab, my_metatable)
-- Set the __index metamethod:
my_metatable.__index = function (tab, key)
print("Hello, " .. key)
return "cruel world"
end
-- Trigger the __index metamethod:
print("Goodbye, " .. my_tab["world"])
In the above example, the goodbye print statement attempts to index world in my_tab . Since that key hasn’t been set in my_tab (it is nil), Lua calls our __index function with my_tab and world as arguments. The function returns the string cruel world , which is returned to the goodbye print statement. This code will output:
Hello, world
Goodbye, cruel world
This example is pretty crucial to understand what’s going on behind the scenes and why. A key feature of __index is that you don’t have to set it to a function. You can also set it to another table instead! The behavior is now as follows: if you index something that isn’t in the table, it checks the table in __index instead (like a backup table).
Why is this important? For our objects, we will be creating a table of the functions (methods) that every object of one kind should have. We’ll set the metatable of the objects to equal this table of functions so that we don’t have to copy functions over to every instance of an object when it is created.
However, before we get to that, we have to understand colon syntax for method calling (remember that a method and a function are essentially the same, except that the word method is used in the context of OOP). In Roblox Lua, you are required to call methods like Destroy by using a colon. How is this different from using a period? When you use a colon in a method call, Lua sends the table itself (the left of the colon) as the first argument to the function. For example, if you had a table foo with a function bar , then calling foo:bar() is the same as foo.bar(foo) . More info about this behavior can be found in Chapter 16 of Programming in Lua. It’s syntax sugar.
When you use a colon in a method call, Lua sends the table itself (the left of the colon) as the first argument to the function.
-- Define a table foo with a function bar inside it
local foo = {}
foo.bar = function ()
print("Hello, world")
end
-- These lines are equivalent.
foo.bar(foo)
foo:bar()
Let’s put this all together: we can store our object’s methods (functions) inside a table. We set this table as the __index for the metatable of newly created objects. We call these methods using colon syntax so our functions can access the object we’re talking about. In the next article, we will take all these abstract topics and apply the concepts to concrete examples.