When to use metatables

Hello developers, I am making this post for some insight. I think I know how to use metatables pretty well, but I can’t really find a use for them ever. This leads me to think that metatables aren’t really that useful. Am I wrong? And if so, please give some examples on use cases for metatables, specific and generic. Thankyou!

4 Likes

If you don’t come across any instances where you need to use metatables, that’s fine. The only time I use metatable is when I’m dealing with OOP (Object-Oriented Programming) or if I’m extending functionality to something like a function.

Here’s what I mean by the latter:

local function main(self)
   print(self.Name)
end

local start = setmetatable({}, {__call=main}) -- set the '__call' metatable to the 'main' function
start.Name = "Roblox" -- add the index to the table
start() -- call the table to execute the 'main' function

Although the times I use anything like the above is quite rare.

For OOP, this post goes into more detail about it:

2 Likes

(Sorry if the post you linked answers this) When should I be using OOP?

1 Like

Most of the time you will use metatables for OOP, but even there it isn’t strictly necessary unless you deal with inheritance which is basically a sin. Other use cases may be for a lazy loading system, for eg calculating the target value of a spring only when you need it, as seen in Quenty’s spring module. Another use case may be for creating a custom datatype such as Vector3 or Quaternion (bad examples don’t try).

1 Like

OOP is used in cases where you want to create re-usable code or custom Lua objects. It’s mainly present in larger code bases (such as Roact).

The idea of Lua/Luau OOP is to replicate classes and objects from other languages such as C#, Java, and C++.

image

(Lua/Luau equivalent):

local dog = {}
dog.__index = dog

function dog.new()
   return setmetatable({}, dog)
end

function dog:Eat()
end

function dog:Drink()
end

function dog:Sleep()
end

But yes, the post I linked goes into far more detail


No

2 Likes

Is using metatables and OOP needed? Are there any good game devs that don’t use it, and does it make me a bad developer if I don’t use it.

1 Like

Actually they are quite necessary, I don’t know why I said otherwise.

Plenty, ik that evaera uses it in her Promises and dphfox uses it in Fusion.

Not at all, it mainly depends on the use case tbh. Though metatables are powerful, they are also easy to misuse.

1 Like

I feel like “metatables” makes it seem more complicated than it actually is.
Metatables are tables within tables.

So you could have something like this:

local table_1 = {
    ["Alphabet"] = {"a", "b", "c"} -- this is another table inside of a table. 
}

And you can access “Alphabet” like so:

local alphabet = table_1.Alphabet

The “OOP” most people talk about are functions which are contained inside tables.

local module = {}

function module.Add(a: number, b: number)
    return a + b
end

This is actually equivalent to:

local module = {}

module.Add = function(a: number, b: number)
    return a + b
end)

Similarly, what most oop modules return is actually a table containing all the methods. The “self” variable is also another table. It’s like a global table but just for that object.

local module = {}
module.__index = module

function module.New(name: string)
     self.Name = name
     self = setmetatable(self, module) -- this adds all the methods to the self table
     return self
end

-- then you can use "self" later on in methods
function module:SayName()
     print(self.Name)
end

In a nutshell, the “self” variable is like having a duplication of the modulescript, but has extra information.

I know I said that I understand metatables well, but what does doing module.__index = module do?

I also thought that you could only use self on the : and not the . (when making the functions)

1 Like

No, you can make your game fully functional - without OOP. OOP is just a style of coding.

Is there a better of the two?

sdfsdfsdfsdf

.__index is basically a “fall back” option when you try to access something that doesn’t exist.

A more intuitive example can be seen here: Programming in Lua : 13.4.1

In the example, we can set a default value for window:

    -- create a namespace
    Window = {}
    -- create the prototype with default values
    Window.prototype = {x=0, y=0, width=100, height=100, }
    -- create a metatable
    Window.mt = {}
    mt.__index = Window.prototype -- look here!
    -- declare the constructor function
    function Window.new (o)
      setmetatable(o, Window.mt)
      return o
    end

If I try and create something with .new(), without the required “width” parameter, it falls back on the .prototype table.

2 Likes

To each their own, I recommend you experiment with both and pick your favourite. You can also mix functional and OOP in your game.

There are certain performance benefits to using functional coding.

2 Likes

It’s routing. Metatables have certain metamethods that allow you to control certain actions within a table.

The __index metamethod is a method that can be used to control the indexing of a table. We can set it to a function and further customize how a table is indexed.
By doing module.__index = module, you are basically creating a route where when the module table is indexed, it will always return the module itself.

You can check out the documentation for more information about metatables.

This code is wrong and will not work. Self is only defined when you initialize a method with “:”.
Here’s a good forum post about how OOP works and what it can be used for:

2 Likes

I admit it! Made a mistake here:

local self = {} -- forgot to define self here!
self.Name = name
self = setmetatable(self, module) -- this adds all the methods to the self table
return self

After fixing it, I ran it in a local script like so:

local module = require(script.ModuleScript)


local animal = module.New("SkrubDaNub")

animal:SayName()

And this is the output:

The : is not mandatory to initiate self.

That is true, but self isn’t really unique as some posts here seem to make me feel. Self is basically just a variable that automatically gets passed to the function if you call it with the colon. You can call the function like module.SayName(module) and it would function identically to using the colon operator instead. Moreover, if you were to define the method without the colon, you can still use it with it, having to define self (maybe even giving it a different name) in the arguments of the function instead.

1 Like

The purpose of self is to make referencing the object easier.
It’s not mandatory, sure, but it’s highly recommended since it’s much easier than passing the object to the function every time you want to run a method. And if you’re working on a let’s say, public resource, asking the developers to pass the object over and over again just to run a method isn’t going to look good.

A lot of things aren’t mandatory and you are free to do whatever you want, but in my opinion, if you want to be a better programmer, you have to learn how to work fast and simplify things.

I never intended to say that you shouldn’t use self or the colon operator, though. You are correct, it does make it easier. My intent was to clarify what self really was.

Good, but a constructor function would be much better in this case.

local module = {}
local methods = {}
methods.__index = methods -- Routing

-- Setting the methods
function methods:setData(name: string, age: number)
   self.Name = name
   self.Age = age
end

function methods:printData()
   print(self.Name, self.Age)
end

-- Constructor
function module.new()
   -- Creating the object
   local object = setmetatable({}, methods)
   
   -- Initializing the data 
   object.Name = "" 
   object.Age = 0
   
   -- Returning the created object
   return object
end

return module
1 Like