The Catcher Code Pattern
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!