Recursive table.freeze

In response to:

Finally, we have a table.freeze


Here’s a quick function so the beginners don’t have to do this themselves (I personally hate writing recursive functions)

local function deepFreeze(tbl)
    table.freeze(tbl)
	for _, v in pairs(tbl) do
		if type(v) == "table" then
			deepFreeze(v)
		end
	end
end

This obviously isn’t tested as table.freeze isn’t out yet. I do hope that freezing a table still lets you freeze nested tables within.

12 Likes

oh so this is what they meant by deep freeze, makes more sense now

1 Like

Just a reminder that this will error and cause a stack overflow for tables with a cyclical reference.

an easy way to prevent this is by having a cache and checking if the table has already been processed

here's an example
local function filter(table: table, cache: table?): table?
	cache = cache or {}
	local new = {}

	cache[table] = true

	for k, v in pairs(table) do
		if typeof(v) == "table" then
			if not cache[v] then
				new[k] = filter(v, cache)
			else
				new[k] = "Cyclic Table: " .. tostring(v)
			end
		elseif Filters[typeof(k)] and Filters[typeof(v)] then
			new[k] = v
		end
	end

	cache[table] = nil

	return new
end
4 Likes

Here’s a better and a reliable solution which accounts for cyclic tables:

local function DeepFreezeTable(tabl)
    table.freeze(tabl)

	for k, v in pairs(tabl) do
		if typeof(v) == "table" and not table.isFrozen(v) then
			DeepFreezeTable(v)
		end
	end
end
4 Likes

The only problem with that solution is that it does not account for a chain of cyclic references of tables.
Consider this scenario:

local function DeepFreezeTable(tabl)
	table.freeze(tabl)
	
	for _, v in pairs(tabl) do
		if typeof(v) == "table" and v ~= tabl then
			DeepFreezeTable(v)
		end
	end
end

local AnArray = {}
table.insert(AnArray, {
    Parent = AnArray
})

DeepFreezeTable(AnArray) --This will go on forever because of lack of caching.

my method is still superior because it is a O(1) Constant time while table.find is O(n) Linear time, the bigger the array is the slower it becomes.

@BenMactavsin thanks :+1:

I don’t recommend doing this variable name
table is already a global

1 Like

Remind you that the difference is n egligibleand the code was edited after @BenMactavsin pointed out an edge case which I forgot to account for due to an oversight.

Due to hash collisions, lookup times will not always necessarily be O(1) but O(n) in the worst case which can potentially happen quite often.

I think it’s worth noting that, in cases where all of a table’s descendants are known not to be frozen prior to the call; table.isfrozen could be used in place of caching.

table.isfrozen Implementation
local DeepFreeze = (function()
	local FreezeNode = function(NodeArray, Node, IsHostNode)
		for Key, Value in pairs(Node) do
			if type(Value) == "table" then
				table.insert(NodeArray, Value)
			end
		end
		if IsHostNode and table.isfrozen(Node) then -- No redundancy; isfrozen is only called when IsHostNode is true.
			return nil
		end
		table.freeze(Node)
	end
	local StepNextNodes = function(CurrentNodes)
		local NextNodes = {}
		for Index, Node in pairs(CurrentNodes) do
			if not table.isfrozen(Node) then
				FreezeNode(NextNodes, Node, false)
			end
		end
		return NextNodes
	end
	return function(HostNode)
		local CurrentNodes = {}
		FreezeNode(CurrentNodes, HostNode, true)
		while #CurrentNodes > 0 do
			CurrentNodes = StepNextNodes(CurrentNodes)
		end
	end
end)()

But of course, that wouldn’t be the ideal functionality if some subtables are already frozen prior to the function call. In that case; you’d either have to precompute the tree, or use a HashTable in order to keep track of cyclic references.

My HashTable Implementation
local DeepFreeze = (function()
	local FreezeNode = function(NodeArray, FrozenNodes, Node)
		for Key, Value in pairs(Node) do
			if type(Value) == "table" then
				table.insert(NodeArray, Value)
			end
		end
		if not table.isfrozen(Node) then
			table.freeze(Node)
		end
		FrozenNodes[Node] = true
	end
	local StepNextNodes = function(CurrentNodes, FrozenNodes)
		local NextNodes = {}
		for Index, Node in pairs(CurrentNodes) do
			if FrozenNodes[Node] == nil then
				FreezeNode(NextNodes, FrozenNodes, Node)
			end
		end
		return NextNodes
	end
	return function(HostNode)
		local FrozenNodes = {}
		local CurrentNodes = {}
		FreezeNode(CurrentNodes, FrozenNodes, HostNode)
		while #CurrentNodes > 0 do
			CurrentNodes = StepNextNodes(CurrentNodes, FrozenNodes)
		end
	end
end)()

I used an inline function in order to define both examples, but it should be preferred to have a ModuleScript make the return instead. I’m also operating on the assumption that table.freeze won’t throw an error when called on tables that have potentially already been frozen.

Edit: So we’ve gotten a bit more information, and it unfortunately seems that table.freeze will indeed error when called on a frozen table. Both implementations have been edited accordingly.

2 Likes

Or they can just create it on their own not knowing that this post exist.

1 Like