Metatables, Metamethods, ModuleScripts and OOP

Introduction

This tutorial will quickly review basic usage of tables and dictionaries and later get into metatables, and how they can be used to better your code.

Pre-Requisites:

  • Tables
  • ModuleScripts

Terminology Used

Array: A collection of various data types put together and ordered using numerical keys.

  • Example: { “Hello” , “World”, true, 27, { } }

Dictionary: A collection of various data types put together using non-numerical keys.

  • Example: {Key1 = “Nice” , EpicKey = “EPIC” , SomeNumber = 27 }

Function: A set of instructions that execute a task, not associated with an object.

  • Example:
local function exampleFunc()
    print("Hello World!")
end

Method: A set of instructions that execute a task, associated with an object:

  • Example:
Part:Clone()

Let’s Get Started!

Before we begin talking about metatables it’s important that we have a solid understanding of tables and dictionaries. In this tutorial we will mainly be focusing on dictionaries for the reason of us being able to have non-numerical keys. Very quickly I will go over dictionaries and how to set one up.

-- you can use ; (semicolon) or , (comma) at the end of each key w/ its value
-- I prefer using semicolons
local myDictionary = {
    A = "Hello";
    B = "World";
    What = 2727;
}

-- "A", "B", and "What" are all keys in the dictionary "myDictionary"
-- Their values are across the equal sign

-- If we wanted to get the value of key "A" we would do this:
print(myDictionary.A) --> Hello
-- We could also do this (same output)
print(myDictionary["A"]) --> Hello

-- If you try to index a key that isn't defined in the dictionary,
-- it will return nil
print(myDictionary.no) --> nil

Above is a very basic explanation of dictionaries and if you are already familiar with them, it would be best to skip it, if not there is no harm in reading!

Metatable Introduction

Before we start discussing the wonderful ways in which metatables can better your code, let’s discuss what they even are.

Metatables really aren’t all that confusing to understand but sometimes people over complicate them when they shouldn’t be. By definition, a metatable is a regular Lua table that can only contain these things called metamethods. Anything that is not a metamethod in the metatable will be ignored and removed from it. It’s probably important to know what a metamethod is then right? A metamethod is an event that is fired when an action is done upon a table. How this is accomplished is by attaching the metatable to a normal table. I will explain a function to attach the metatable after I list the various metamethods.

All Metamethods


The main method we will be focusing on in this tutorial is the __index metamethod. Feel free to mess with the other’s but this tutorial will not go over them.

Attaching a Metamethod

In order to attach a metatable to a table or dictionary we need to use a global Lua function called setmetatable. The wiki image will explain it below, but I will explain it further after.


setmetatable takes two parameters, the first being the normal table or dictionary we want to attach the metatable to, the second being the metatable we are attaching. So it would look something like this:

local myTable = { Value = "Nice" }
setmetatable(myTable,{})

--Alternate way to do it:
local myTable = setmetatable({ Value = "Nice" },{ })

Using the __index Metamethod

As said before I will only be using the __index metamethod in this tutorial as it will be useful later when we get into a coding that relates to Object Oriented Programming. For now I will explain how this metamethod works and how to use it.
According to the API, this metamethod does this:


If you don’t understand how the wiki explains it, let me try to clear things up for you.

Let’s say we have a dictionary called “myDictionary” and we set a couple keys for said dictionary:

local myDictionary = {
    A = 42;
    B = true;
}

Now let’s say we want to get the value of the key “A”. We would have to index the key in the dictionary in order to get it’s assigned value. Like this:

local myDictionary = {
    A = 42;
    B = true;
}

local whatWeWant = myDictionary.A
print(whatWeWant) --> 42

Now look back at the description of the __index metamethod, this method is fired when you try to index something in a table ONLY if that key you try to index is nil. So if we did this:

local myDictionary = {
    A = 42;
    B = true;
}

local whatWeTry = myDictionary.why -- the __index metamethod will fire
print(whatWeTry) --> nil

The only issue with the example above is we assumed there was a metatable attached to the normal table. Let’s have an example where I actually attach a metatable to a table with the __index metamethod inside of it:

-- first I create the normal table
local myTable = {
    A = "Epic";
    B = true;
}

-- now I will attach a metatable to the normal and have an __index metamethod inside of it
setmetatable(myTable,{
    __index = function(normalTab,attemptedKey)
        print(normalTab,attemptedKey)
    end
})

-- now if I try to index a nil key this will happen
local myKey = myTable.noExist
--> __index fires > table_address "noExist"
print(myKey) --> nil

As you can see the __index metamethod takes 2 parameters: the normal table and the key you attempt to index. In the function body I choose to print out the parameters.
Please do mess around with this knowledge, but know I will be getting into a more important aspect of the __index metamethod. Notice how I am setting it to a function, but you can also set it to another table. If the key you index in the first table is nil, and your __index metamethod is set to another table, it will attempt to find said key in the second table before returning nil.
For example:

local firstTable = {
    A = "Epic";
    B = true;
}
local secondTable = {
    C = "Woah";
}

setmetatable(firstTable,{
    __index = secondTable;
})

-- now if we try to index a nil value in the first table
-- it will check the second table to see if it exists there
local value = firstTable.C --> __index metamethod fires
print(value) --> Woah

Alright awesome! Now that we have accomplished this I can finally show you how these metatables and metamethods can be useful.

First we Need ModuleScripts

image
If you aren’t familiar with what a ModuleScript is, let me briefly explain it. A ModuleScript is a type of script object that was made to return data when retrieved using a require function. For example:

--ModuleScript
--This is what you will see inside a newly created ModuleScript
local module = {}

return module
--(Normal)Script
--Now let's get the data from that ModuleScript
local myModule = require(path_to_module)

The variable “myModule” is now equal to the table that we return inside the ModuleScript. We can return other things besides tables, like strings, numbers, functions, etc. but the most useful return value to use are tables and dictionaries.

Object Oriented Programming (OOP)

According to Wikipedia, “Object-oriented programming is a programming paradigm based on the concept of “objects”, which can contain data, in the form of fields, and code, in the form of procedures.”
To explain that in more simpler terms, in OOP, everything is considered as objects, and those objects are created using classes. If you think about it, this is a very good approach because, all objects in the real world have properties about them that make them what they are. In programming, we can code objects to have properties (with variables), states (with variables), and behaviors (with methods).

Now you may be asking, well how do I create objects. And there is a very simple answer: dictionaries. It would be impractical to use arrays, because of the fact they have numerical ordered keys. But with dictionaries, there can be non-numerical keys, which is good if we want to have custom properties and methods.

Let’s dive straight into creating objects with tables, and we are going to do that with ModuleScripts. Here’s how:

--ModuleScript
local Object = {}

return Object

Alright great, so we made a ModuleScript containing a table called “Object”. Next I will demonstrate how the __index metamethod will play a role.

--ModuleScript
local Object = {}
Object.__index = Object

return Object

You may be confused by what I just did, but remember how we can set the __index metamethod to a table, well I basically just created a new key for the metamethod and set it to the table we have created called “Object”. You may be questioning why I do this, but let me continue further with code so it hopefully makes more sense.

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()

end

return Object

We have now created a new function key in “Object” called new. This term is generally used when creating new objects, even in other programming languages.

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()
    local newObject = setmetatable({ },Object)
    return newObject
end

return Object

Alright so you may be confused on what I just did, but let me explain. As mentioned before, the setmetatable function is used to attach a metatable to the normal table, the normal table being the first parameter, and the metatable being the second. It also returns the normal table with the attached metatable. In the code, I attach “Object” to a new blank table I create, and then return that table at the end.
The question now is, why do I set the metatable of the new table equal to Object. Remember how I said, metatables can only contain metamethods, and any other key that is not a metamethod will get ignored and become nil. Well in our situation, at the top of the code, I set a new metamethod key of __index equal to the Object table.
Next I want you to remember how when the __index metamethod is set to another table, if you try to index a nil key in the first table, it will attempt to find said key in the table you set the metamethod to.
This is great because now we can index keys in our newly created table and have them search the Class to see if it exists. Notice how I am calling the Object table a class now. A class is basically the blueprint for an object.
Here is how this can be useful:

--ModuleScript
local Object = {}
Object.__index = Object

Object.SomeProperty = 27

function Object.new()
    local newObject = setmetatable({ },Object)
    return newObject
end

return Object
--(Normal)Script
local Object = require(path_to_module)

--now lets create a new object with our "new" method
local newObject = Object.new()

--now we will attempt to find "SomeProperty" inside of the newObject
print(newObject.SomeProperty) --> 27
--this successfully prints because "SomeProperty" is a key inside of the 
--Object table which is what __index is set to in the newObject's metatable

We can also apply this knowledge to methods, which may look a little more useful:

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()
    local newObject = setmetatable({ },Object)
    return newObject
end

function Object:SayHello()
    print("Hello there!")
end

return Object
--(Normal)Script
local Object = require(path_to_module)

local newObject = Object.new()
-- this successfully prints because "SayHello" is a method of the Object table
-- and thanks to our __index metamethod
newObject:SayHello() --> Hello there!

Creating Properties in the New Object

Besides creating new keys in the Object table and having the __index metamethod handle finding those keys, we can also create them in the “newObject” table, like so:

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()
    local newObject = setmetatable({ },Object)

    newObject.IsEpic = true

    return newObject
end

function Object:SayHello()
    print("Hello there!")
end

return Object

Now we get the “property” in a normal script, like so:

--(Normal)Script
local Object = require(path_to_module)

local newObject = Object.new()
print(newObject.IsEpic) --> true

One major issues that comes up is, what if we want to access our newObject we create inside of our Object methods. This can be done easily using the keyword: “self”. It is pretty self explanatory (pun intended), but it refers to itself, or the table in which we are calling the method upon.
For example, let’s say we want to return the “IsEpic” property in a method called “GetEpicStatus”.

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()
    local newObject = setmetatable({ },Object)

    newObject.IsEpic = true

    return newObject
end

function Object:GetEpicStatus()
    -- self refers to the table that calls this method
    -- since __index exists allowing for us to call this method upon newObject
    -- self would refer to the newObject
    return self.IsEpic
end

return Object
--(Normal)Script
local Object = require(path_to_module)

local newObject = Object.new()
print(newObject:GetEpicStatus()) --> true

Using a : (colon) VS . (dot)

When creating methods for our “Object”, there are actually two ways of going about doing so. You can either use a : (colon) or a . (dot). For example:

--ModuleScript
local Object = {}
Object.__index = Object

function Object.new()
    local newObject = setmetatable({ },Object)

    newObject.IsEpic = true

    return newObject
end

function Object:GetEpicStatus()
    return self.IsEpic
end

function Object.GetEpicStatus(self)
    return self.IsEpic
end

return Object

This person below explains it very well and I couldn’t have explained it better myself.


Credit to @Isocortex
In our script, however, the normal script would look something like this:

--(Normal)Script
local Object = require(path_to_module)

local newObject = Object.new()
print(newObject:GetEpicStatus()) --> true
print(newObject.GetEpicStatus(newObject)) --> true

Please be aware if you are using a colon or dot, because you cannot use a colon is you use a dot and vice versa.

Example of Actual Usecase

I am going to very quickly setup a ModuleScript and use OOP to create, load, and manage data using the knowledge we have learned. I am not going to be very detailed or complete with the code, because I want you all to experiment on your own and find the best way to go about doing things with your own experience.

--ModuleScript
local default_data = {}

local PlayerData = {}
PlayerData.__index = PlayerData

function PlayerData.new(Player)
  local self = setmetatable({},PlayerData)
  
  self.DataKey = Player.UserId
  self.Session = {
    Data = default_data
  { 

  return self
end

function PlayerData:Load()
  -- load the player data, do anything you need to do like actions upon the player based on the data they have saved
end

function PlayerData:EnableAutoSave()
  -- resume an auto save thread
end

function PlayerData:DisableAutoSave()
  -- pause an auto save thread
end

function PlayerData:Save()
  -- a general save method using datastores
end

return PlayerData
--(Normal)Script in ServerScriptService
local Players = game:GetService("Players")
local PlayerData = require(path_to_module)

local AllPlayerData = {}

Players.PlayerAdded:Connect(function(Player)
  local newPlayerData = PlayerData.new(Player)
  AllPlayerData[Player.UserId] = newPlayerData
end)

Players.PlayerRemoving:Connect(function(Player)
  local pData = AllPlayerData[Player.UserId]
  if pData then
    -- a little different on how i save it but you get the point
    pData:Save()
    AllPlayerData[Player.UserId] = nil
    pData = nil
  end
end

Conclusion

If you made it this far, thank you for reading my first ever tutorial on the DevForum. Please feel free to correct any mistakes I have made, I have proofread this post, but have probably missed something due to its size. Also if I explained something in the wrong way please let me know in the replies and I will try to correct it as soon as I can.
Thank you :smile:

Extra Resources
https://developer.roblox.com/en-us/articles/Metatables

37 Likes

Just one quick correction:

That’s an array. A table can have any (non-nil?) type for keys; even a function if you really wanted. A dictionary is a type of table, for the record.

2 Likes

Thank you! I have changed it from “Table” to “Array”, under the Terminology section.

1 Like

Dictionary: A collection of various data types put together using non-numerical keys.

Technically non-numeric keys can still be part of the dictionary when the key is outside of the integer range [1, #tbl].

Example:

local x = {
  [5] = true
}

Is a dictionary (and not an array!).

By definition, a metatable is a regular Lua table that can only contain these things called metamethods. Anything that is not a metamethod in the metatable will be ignored and removed from it.

False. As you said in the first sentence a metatable is a regular Lua table. You can put anything you want in a metatable and it won’t be removed.

normal table or dictionary

Don’t differentiate, this makes no sense. Dictionaries are “normal tables” and so are arrays. Also, you use the term “normal tables” but they’re just tables so say tables.

ONLY if that key you try to index is nil

ONLY if the value mapped to be the key you use to index the table is nil. Table keys can’t be nil (or nan).

I didn’t really finish the article, but Magnalite already wrote a good article about OOP here: All about Object Oriented Programming.

1 Like

You’re missing a quite few metamethods:
__shr, __shl, __bnot, __bxor, __bor, __band, __idiv, __pairs, and __ipairs

All of them can be found on the lua docs

But Luau doesn’t have these so what is the point?

1 Like

bit32 operations don’t fire these? And what about idiv, pairs and ipairs?

Seems like they didn’t add these metamethods on purpose


Adding operators along with metamethods for all of them increases complexity, which means this feature isn’t worth it on the balance.

Here they said no to metamethods for bit32 operations as well.
Source: https://roblox.github.io/luau/compatibility.html

Would be really nice for them to include that on the wiki because I’ve never even seen any links to that site from the wiki or the devforum before today. Didn’t even see it from their post talking about bit32.

1 Like

probably slightly unrelated, but even on the website, they have no mention of table.insert() not firing __newindex.

It would make sense that they didn’t add it for compatibility reasons, but I don’t like how they’re just leaving us to guess. I would also prefer it having a section to itself on the developer docs, since I didn’t know about the website either until last month

1 Like

I will be sure to fix my tutorial as soon as I get the chance, thank you for this awesome response :smiley:
Also I know tutorials similar to this one already exist, but I wanted to share my own take on the subject.

1 Like

Wow. That has to be the most helpful, in-depth tutorial I have ever read on dev forum.

Btw there is a tiny typo in this sentence:

1 Like

This is amazing. Really helped me, thanks! I would recommend this to anyone trying to learn metatables and OOP. Amazing Tutorial! :slight_smile:

1 Like

What is OOP?
I don’t understand what is an OOP?

I have a question. Do you always have to use setmetatable to attach a metamethod to a table?

Can I do:

local myTable = {}

myTable.__index = myTable

Yes, or how will you tell Lua(u) that you want to invoke a metamethod? What if you just want a key called __index but not actually use it as a metamethod?

1 Like

This is an old tutorial of mine, I will not be updating this thread anymore because of my new one shown below: