background-shape
feature-image

Background

While developing my game framework, I came across a unique code pattern I hadn’t seen documented elsewhere, which acts like an infinitely recursive proxy. So, I decided to flesh it out, giving it get, set, and call functionality, as well as state retrieval, so that it functions indeed like an infinitely deep hierarchichal object mask.

Here is the complete source code, for what I am calling a Catcher:

--!strict
function appendTo(t, v)
	local _t = table.create(#t+1)
	_t[#t+1] = v
	return table.move(t, 1, #t, 1, _t)
end

function catcher(called, set, got, path)
	return setmetatable({}, {
		__call = function(_, ...) return called(path, {...}) end; 
		__index = function(_, i) return catcher(called, set, got, appendTo(path, i)) end; 
		__unm = function(_) return got(path) end;
		__newindex = function(_, i, v) set(appendTo(path, i), v) end; 
	}) 
end

function new(called, set, got)
	return catcher(called, set, got, {})
end

return {new=new}

Use Case

Before I delve into a full breakdown of the code, I think it’s always useful to have a use case in mind when applying a code pattern. But this isn’t me telling you how to use it! Go wild.

In my framework, I am using this in conjunction with Evaera’s Promise library, to completely desynchronise usually synchronous actions such as data retrieval. No longer do you have to yield your whole thread to await data that needs to be indexed.

Instead, assume it exists, because you know it does, and have specified so in your data structure, and then index a DataCatcher, an instance of Catcher, that will then use its get method and return you a Promise that will resolve once the data becomes available, or error if the data becomes availiable without the field you required existing. Here’s a more tangible example of the beauty that this code pattern allows:

local ServerData = {
	Fish = xx;
	BananaBreadStorageUnit = {
		BananaBreadSupply = xxx;
		BananaBreadBackupSupply;
	}
}

No longer do you need a single module to handle the breakdown of this data. Oh no, you can handle it from any module you like, and use my the DataCatcher to request the single path you require. And it’s as easy, idiomatic and verbose as the following:

local Module = {} 
-- Automatically receives several Catchers that are
-- injected when the module is initialised by the framework

--[[ rest of your code here ]]

-- And now you need some data? Let's request it!!
local supply = -Module.Data.BananaBreadStorageUnit
	.BananaBreadSupply
-- Unary negation is the catcher's get
supply:andThen(function()
	...
end):catch(function()
	...
end)

And no matter whether the data has loaded at the time you attempted to index it, it will get resolved in due course, and everything will run smoothly, without any prior knowledge at runtime by the codebase of the structure of the data to be resolved. This is the basis of my framework, and allows for a clean Network abstraction, as well as Data abstractions and an internal module hierarchy that allows me to enforce SRP, clean decoupled code, and an amazing code-style sure to revolutionise your workflow!

Analysis

Hopefully now that you have an appreciation on how this code pattern can revolutionise most user-facing applications interaction system, we can go ahead and break it down into its core components!

--!strict
function appendTo(t, v)
	local _t = table.create(#t+1)
	_t[#t+1] = v
	return table.move(t, 1, #t, 1, _t)
end

function catcher(called, set, got, path)
	return setmetatable({}, {
		__call = function(_, ...) return called(path, {...}) end; 
		__index = function(_, i) return catcher(called, set, got, appendTo(path, i)) end; 
		__unm = function(_) return got(path) end;
		__newindex = function(_, i, v) set(appendTo(path, i), v) end; 
	}) 
end

function new(called, set, got)
	return catcher(called, set, got, {})
end

return {new=new}

For your convenience, here’s the source code again!

So, a Catcher is merely a metatable, that returns another copy of itself, with an updated path whenever it is indexed! To avoid issues with path mutability, it’s necessary to use appendTo to append a value to the current path and then return a new path, so that we don’t end up with a reference to the old path in the new catcher!!

Now, if we look at each metamethod individually, we can see have an even more intuitive breakdown!

__call = function(_, ..) return called(path, {...}) end Here the code is escaping the catcher, and giving pack both the current index path and the arguments passed to the function, to the called handler!

__index = function(_, i) return catcher(called, set, got, appendTo(path, i)) end; Here, the code is returning another catcher, when it gets indexed, with an updated path, and this is one of the key recursive steps!

__unm = function(_) return got(path) end; Here, the code is treating unary negation as a get call. Since there is no __get metamethod, we can’t know for sure when a table has been “got” per se. So, I used unary negation here to mimic the same thing!

__newindex = function(_, i, v) set(appendTo(path, i), v) end; Here, the code exits again, and passes an updated path and newvalue to the set function!

In conjunction, these methods allow you to do all the following:

local a = Catcher.new(x,y,z)

a.call.a.function(with, arguments)
get_a_promise_from = 
	a.call.a.function(with, arguments)

get_a_promise_from =
	-a.get.a.value

a.assign.to.it = newvalue!

Conclusion

Thanks for reading my first ever blog post and I hope you now have an appreciative understanding of how to use and abuse the Catcher code pattern, and I can’t wait to see what you can do with it!

Send me your suggestions, creations, and questions at Inctus#7666 on Discord!