Module script and metatables

I found this script on this page Metatables :

local Set = {}
Set.__index = Set

-- Function to construct a set from an optional list of items
function Set.new(items)
	local newSet = {}
	for key, value in items or {} do
		newSet[value] = true
	end
	return setmetatable(newSet, Set)
end

-- Function to add an item to a set
function Set:add(item)
	self[item] = true
end

-- Function to remove an item from a set
function Set:remove(item)
	self[item] = nil
end

-- Function to check if a set contains an item
function Set:contains(item)
	return self[item] == true
end

-- Function to output set as a comma-delimited list for debugging
function Set:output()
	local elems = {}
	for key, value in self do
		table.insert(elems, tostring(key))
	end
	print(table.concat(elems, ", "))
end

The thing is that when you do that thing u are assigning all of the methods to the new Set created, so you can do : mySet.new() but its a non-sense.

So my question is, are metatables useful to create such things as this, I mean create kind of objects.

Thanks you for the reply and have a nice day !

1 Like

Just because it is possible does not mean it is recommended. Being able to call .new() from an already constructed object is not the intended way to use that function. It is just a byproduct of setting the __index table of the object’s metatable to the class table.

1 Like

Yes but i’d like to make something clean, as non-scripters will have to use the module.

I want .new() to be used only from the module, then i want to add functions to my new objects.

What do u think would be the best way to create objects like that ?

I thought about creating a module for the object and another for creating the objects. The problem is with the assignment of custom value with the .new() function params.

What I did so far is that I created a table in the .new() function and then add my functions to it and values and then return it.

Thanks you for the reply !

1 Like

I suppose you could have a prototype table for object methods. Something like this would do:

local Animal = {}
Animal.prototype = {}
Animal.prototype.__index = Animal.prototype

function Animal.new(name)
	local self = {}
	setmetatable(self, Animal.prototype)

	self.name = name
	self.hunger = 0
    return self
end

function Animal.prototype:feed(amount)
	self.hunger = math.max(self.hunger - amount, 0)
end
2 Likes

Smart !

But is it really better than doing that :

function Animal.new(name)
  local newAnimal = {}
  
  newAnimal.name = name
  newAnimal.hunger = 0

  function newAnimal:feed(amount)
    newAnimal.hunger = math.max(newAnimal.hunger - amount, 0)
  end

  return newAnimal
end

It is what im doing rn

1 Like

I think it is better. With your method, you’re creating a new table of methods instead of reusing them. With my method, the prototype table is used by all objects since they all need the same table of functions/methods.

2 Likes

Correction. In both methods, you are still creating a table, however, you got to be careful when constructing functions inside your new constructor.

Functions with varying outside references will create a new function under a different memory address.

local module = {}

function module.new()
	local internal = {}
	
	function internal.test()
		print(internal)
	end
	
	return internal
end

return module

-- script

local module = require(script.ModuleScript)

print(module.new().test)
print(module.new().test)

-- function: 0xf564924122dfa161
-- function: 0x062626efe6682481

The upside that most people don’t know about the standard pattern is that the variables in the new constructor are sandboxed. To fix this issue, be sure to write code as you would outside of the new constructor.

local module = {}

function module.new()
	local internal = {}
	
	function internal.test(self)
		print(self)
	end
	
	return internal
end

return module

-- script

local module = require(script.ModuleScript)

print(module.new().test)
print(module.new().test)

-- function: 0x350e7b632808354b
-- function: 0x350e7b632808354b
2 Likes

Sorry, but i dont understand at all what ur talking about, u mean the second method doesnt create a new object ?

In simpler terms:

The first code creates an entirely different function due to the reference of internal, something that the function “shouldn’t know” and differs every time new is ran, meaning it has to create a new function to essentially “keep track” of that variable.

The second code does not create a new function as we are passing self, which is the standard OOP usage. Now the function can operate on the passed self without any other outside references it shouldn’t know of. Roblox internally “reuses” the same function for all of the new constructor since it is “similar”.

Here is an interesting read. I recommend if you have the time to read up on the entire website if you are interested: Performance - Luau

2 Likes

It’s crazy, u mean just by using internal in the function it make a new function, but if we use self it dont ?

So it’s better to use self than the table reference we created ?

That is correct, that is why you need to be careful creating functions inside constructors if you want to reduce as much memory usage as possible.

Generally, it shouldn’t matter in a real game environment if classes come and go all the time, it will eventually get garbage collected if there are no other hard references. However, it is one small change that can save even just a little bit on memory if you are creating a lot of these classes or holding on to these classes for a very long time.

1 Like

Yes, I mean I’m creating a dialog class and the game is mostly based on dialogs.
I will have to struggle a bit more on this cuz its a bit confusing to me rn lol

Thanks you very very much, have a nice day !!!

No problem. Be sure to mark something as a solution so this thread is closed haha.

Just to clarify, only references that differs per function creation will create a new function object. This will still work since the OUTSIDE_VARIABLE is the same for all created classes every time the new constructor is ran. We call this “semantically identical.”

local OUTSIDE_VARIABLE = math.random()

local module = {}

function module.new()
	local internal = {}
	
	function internal.test()
		print(OUTSIDE_VARIABLE)
	end
	
	return internal
end

return module
1 Like

So if i’m using arguments in my function test it will create a new function ?

No, it will still be the same function because you are giving it the instructions directly through arguments instead of having the function “keep track” of changing references. self is also an argument automatically passed with : in this case.

local module = {}

function module.new()
	local internal = {
		value = 0
	}
	
	-- same memory address, will not create a new function every time
	function internal:incrementValue(number)
		self.internal.value += number
	end
	
	return internal
end

return module
1 Like

I meant this :

local module = {}

function module.new(name)
	local internal = {}
	
	function internal:test()
		print(name)
	end
	
	return internal
end

return module

In this case, yes, that is correct. This is why most OOP classes will keep all used values inside the table directly for 2 main reasons:

  1. The self argument will have these accessible if constructed with the standard Class.__index = Class and setmetatable({}, Class) approach and the functions are defined outside of the new constructor. Otherwise, they have no methods of grabbing these values.
  2. As stated above, although your average developer doesn’t know about it, it saves on memory since it is stored in the self variable.

To fix your issue, simply store name inside internal.

Edit: The newAnimal code that you posted above, simply changing newAnimal to self inside your feed function is the easiest solution to this. Once again, code it as you would if you were constructing functions outside of the new constructor, where the variables aren’t readily available to you without the use of self.

1 Like

Thanks u again so much for the help !

I still have a question, how could I make this readable, I mean there will be like tons of functions in the new class and in the module (i tried to make a main module and a module only for the class but it seems not creating a new class each time but just updating values in it) ?

Although not suited for this category and topic anymore, I would recommend using whichever method you find more comfortable. I personally construct mine the same as yours (without the use of metatables) and it is very readable for me.

Otherwise, some popular modules out there uses @OptimisticSide’s method with a prototype table, which is a good way if you don’t mind the use of metatables. It is also a good way to differentiate from the main module functions from the class functions themselves.

You can technically separate your class functions inside another ModuleScript, however, depending on how you construct it, you would either still have to use a metatable to get intellisense support* or manually give it support.

* I believe this is the case at least

1 Like

Would you mind if I contact you via messages ?

About the module stuff, I can’t figure out how to make separated module (main module and class module).

Thanks you very much and have a nice day or night !