Maid cleanup for custom objects

Is there any way to reproduce a maid cleanup behavior for custom objects that results in the table being garbace collected?

So essentially I am trying to find a cleaner/sugar solution to this:
local objects = {}

objects[key] = CustomObject.new() -- pretend this returns a table

-- cleanup
for index, object in pairs(objects) do
objects[index] = nil -- the reference is removed and the table GCed
end

If I were to use Quenty’s Maid solution it could look like this:
local someObject = CustomObject.new()

local maid = Maid.new()

maid:AddTask(someObject)

-- cleanup
maid:DoCleanup() -- obviously doesn't work because someObject is now a reference in the internal table of the Maid class which doesn't remove the original

Since I also have connections and userdata objects to clear once a certain event takes place it would make the code a lot cleaner and manageable

Why do you need to do this? If you disconnect all the connections in your table it will be GC’d when there are no other references to it anyway.

See original post, blah blah 30 character limit

I think you’re overcomplicating the thinking behind this or focusing too much on making your code pretty that you’re losing out on functionality.

Just let unused variables go out of scope, such as through a do-end block. You could also add a custom Destroy method, as DoCleaning will call destroy (in object notation via colon) if it exists as a member function. The Destroy method could handle cleanup of your object and it’s internal items.

The first one is fine enough. In the second case, just don’t hold a reference if you don’t need one. Maids are meant to clean up and finish all tasks, often at times where you don’t require references to items you give them.

local maid = Maid.new()

maid:AddTask(CustomObject.new())

maid:DoCleaning()

Custom objects should be managed yourself without maid. Custom objects can have maids on the other hand which handles their items and connections.

1 Like

All you need to do is to modify the Maid class Do cleanup function so that it cleans up objects differently, for example the:

--the maid class do cleanup is prob look something like this
function Maid:DoCleanup()
      for _,Connection in pairs(self.Connections)do
          Connection:Disconnect()
      end
end
--just modify the loop to check if its a table all custom object are tables
function Maid:DoCleanup()
      for _,Connection in pairs(self.Connections)do
           if typeof(Connection) == "table" then
               --destroying the custom object
               for i,v in pairs(Connection) do
                   Connection[i] = nil
               end
           elseif typeof(Connection) == "RBXScriptConnection" then
               Connection:Disconnect()
           end
      end
end

i just wanna add that this isnt the purpose of a maid class, its purpose is to just disconnect functions . the proper way to destroy custom objects is to create your own function in the object to destroy it like for example:

local object = {}
function object:destroy()
   for i,v in pairs(self) do
     self[i] = nil
   end
end
-- then to destroy u do:
object:destroy()

You can use the __mode metatable value for this.
Example:

setmetatable(tbl, {
    __mode = "kv" -- Keys and values will be weak meaning nothing in the table holds a reference. As long as you set references to the keys and values to nil everything in the table will be GCed despite the thread remaining alive.
})

My solution to this would be setting the __mode metatable value to kv and setting the table reference to nil. This ensures that the entire table is GCed.

Secondly you can use secondary references like this to prevent values from being actually referenced allowing you to clean them up:

local reference = {value} -- To GC this you can set the __mode metatable value
value = nil

print(reference[1])
2 Likes

Doesn’t appear to work unless I’m missing something:

local tbl = {"A"}
local reference = {tbl}

setmetatable(tbl, {
    __mode = "kv"
})

tbl = nil

print(reference[1])

The garbage collector only runs every so often.

Reference is a table with strong values, so tbl shouldn’t be removed, if you want it to be removed, then reference should have the weak keys and values, instead of tbl.

local weak = setmetatable({{}},{__mode="v"})
print(weak[1]) --> table: xxxxxxxxx
wait(2) -- wait long enough for a garbage collection cycle, since in roblox you can't force a garbage collection cycle
print(weak[1]) --> nil
1 Like

Do you want garbage collection, or some cleanup routine? I have to imagine the cleanup routine is much more helpful.

Take note of this line of code: NevermoreEngine/Modules/Shared/Events/Maid.lua at 0c2e68aca9e3d299cd26f161768ddc6bab61a422 · Quenty/NevermoreEngine · GitHub

If you define a “Destroy” method on your object, that method will be fired when the maid gets cleaned up or you wipe out the entry.

I’m aware.

See my issue is that I have a table nested within a table (custom object stored within an array).

If you want to clear an object stored within a table you’d set the __mode value of your object to “kv” to set weak keys and values and then clear the value from the table. You can see the effects of the garbage collector by doing wait(1.5) or something along those lines.

The way I made my maid is by giving it a function to run that does the cleaning instead of supplying an (object, method, args) type of deal, which is incredibly flexible, lightweight, doesn’t need type checks, and is great for things that aren’t objects.
My maid module looks like this:

local Maid = {}
Maid.__index = Maid

-- creates a maid
function Maid.new()
	return setmetatable({ Tasks = {} }, Maid)
end

-- assigns maid with a task
function Maid:Give(task)
	table.insert(self.Tasks, task)
end

-- does the tasks assigned
function Maid:Clean()
	for _, task in ipairs(self.Tasks) do
		xpcall(task, function(msg)
			warn(debug.traceback(msg))
		end)
	end
	self.Tasks = {}
end

return Maid

And in practice, it would look like this:

-- make a maid
local maid = Maid.new()

-- do yo thang
local var = "a value"
local t = {1, 2, 3}

local part = Instance.new("Part")
local touchedConn = part.Touched:Connect(print)
-- after doing the thangs,
maid:Give(function()
	var = nil
	t = {}
	
	part:Destroy()
	touchedConn:Disconnect()
end)
-- when you want all yo thangs to get cleaned up,
maid:Clean()

This design pattern could be even more generalized as more of a place to dump functions with “workloads” rather than cleaning tasks, which is exactly what bindable events do:

-- new
local bindable = Instance.new("BindableEvent")

-- give
bindable.Event:Connect(task)

-- clean
bindable:Fire()
bindable:Destroy() -- for disconnecting the events

-- in module form
local RunService = game:GetService("RunService")

local Connection = {}
Connection.__index = Connection

function Connection.new(listeners)
	local self = {
		Connected = true,
		Listeners = listeners
	}
	
	return setmetatable(self, Connection)
end

function Connection:Disconnect()
	self.Connected = false
	self.Listeners[self] = nil
end

local Event = {}
Event.__index = Event

function Event.new()
	local self = {
		Listeners = {},
		Waiting = {}
	}
	
	return setmetatable(self, Event)
end

function Event:Fire(...)
	for _, fn in pairs(self.Listeners) do
		coroutine.wrap(fn)(...)
	end
	
	for _, thread in pairs(self.Waiting) do
		coroutine.resume(thread, ...)
	end
	self.Waiting = {}
end

function Event:Connect(fn)
	local connection = Connection.new(self.Listeners)
	self.Listeners[connection] = fn
	return connection
end

function Event:Wait(timeout)
	local key = newproxy()
	local thread = coroutine.running()
	self.Waiting[key] = thread
	
	if timeout then
		coroutine.wrap(function()
			local i = 0
			while i < timeout do
				i += RunService.Heartbeat:Wait()
			end
			self.Waiting[key] = nil
			coroutine.resume(thread, i)
		end)()
	end
	
	return coroutine.yield()
end

return Event

This is obviously the same as just adding a destroy method to an object or a done method to a state, this is just more generalized toward more varied concepts.
To be honest, I will probably be using the event abstraction in my project now, it just seems like too good an idea.

2 Likes