The Scoop
A while back, I created a custom table wrapper called FakeTable for my OOP library and ECS implementation (which was a terrible idea in hindsight). After forgetting about it for a month, I stumbled upon the wrapper again while reviewing old code and decided to make it a resource after some modifications.
Ok, What Even is This Anyways?
FakeTable serves as a table wrapper, intended to replicate the behaviors of metatables along with a few extra features.
FakeTable adds two new custom events (what lua calls things such as __index
) plus a default but currently non-functional one: __get
, __set
, and __len
. __get
works exactly like __index
, except it works only for non-empty fields. The same is true for __set
and its counterpart __newindex
. __len
should behave exactly as it would for normal tables/userdatas.
FakeTable also allows using a custom t1
(or self
as I like to call it) for additional control over metamethods (I don’t know how to make this trick work with normal functions though, so self
still always gives you the FakeTable object). fakeSelf
by default is the original table you give to the constructor.
That means that
ft:foo()
hasft
as itsself
argument, rather thanfakeSelf
. However,metatable.__tostring = function(self)
would have itsself
replaced byfakeSelf
.
Documentation
--[[
Most of ftable's methods are carried over from table or Lua because they
don't work with FakeTable, so I'll just mention them here since usage
should basically be the same.
.pairs()
.ipairs()
.getn()
.insert()
.remove()
.sort()
.clear()
.find()
]]
-- Constructs a FakeTable object
ftable.wrap(meat, metatable, fakeSelf)
-- Use for printing an ftable state
ftable.print(...)
Ok, Is There a Catch?
You bet! The construction of FakeTables is very slow, about 4x slower than using Instance.new("Part")
. FakeTable is also a “double wrapper” because it wraps a metatable which wraps around an table, making it a little bit slower than a normal table. This can be mitigated, but I didn’t spend the time to do so yet.
Other Problems
- Using methods such as
ftable.insert
andftable.remove
do not call any metamethods. - Minimal testing. Potentially has tons of unknown bugs/unintended behaviors
Source Code:
Put this in a ModuleScript
:
-- FakeTable --
local primeKey = newproxy()
local ftable = {}
local className = "FakeTable (Class)"
local function extract(fakeTable)
return fakeTable[primeKey]
end
local function fakeTableComMethod(func, ...)
local to = {...}
return function(fakeTable, ...)
local t = extract(fakeTable)
return func(t, ...)
end
end
--add modified table methods to ftable
local ftable = {
pairs = fakeTableComMethod(pairs),
ipairs = fakeTableComMethod(ipairs),
getn = fakeTableComMethod(table.getn),
insert = fakeTableComMethod(table.insert),
remove = fakeTableComMethod(table.remove),
sort = fakeTableComMethod(table.sort),
clear = fakeTableComMethod(table.clear),
find = fakeTableComMethod(table.find),
}
-- Creates a metamethod wrapper
local function dwrapMm(meat, metatable, self)
return function (event)
return function(_, ...)
local metamethod = metatable["__" .. string.lower(event)]
if metamethod then
return metamethod(self, ...)
else
error("Event '.__" .. event .. "' has no metamethods attached")
end
end
end
end
function ftable.wrap(meat, metatable, self)
local skin = newproxy(true)
local skinId = tostring(skin)
local s_metatable = getmetatable(skin)
--what 'self' to give for metamethods
self = self or meat
assert(typeof(meat) == "table", "meat must be a table")
assert(typeof(metatable) == "table", "metatable must be a table")
assert(getmetatable(meat) == nil, "meat can not have a metatable")
local wrapMm = dwrapMm(meat, metatable, self)
s_metatable.info = {
meat = meat,
metatable = metatable,
}
s_metatable.__index = function(_, k)
--used for protected access to contents
if k == primeKey then
return meat, metatable
end
local v = rawget(meat, k)
print(metatable)
if v ~= nil and metatable.__get then
return metatable.__get(self, k)
elseif metatable.__index then
local __index = metatable.__index
if type(__index) == "function" then
return __index(self, k)
elseif type(__index) == "table" or type(__index) == "userdata" then
return __index[k]
end
end
return v
end
s_metatable.__newindex = function(_, k, v)
local old_v = rawget(meat, k)
if old_v ~= nil and metatable.__set then
metatable.__set(self, k, v)
return
elseif metatable.__newindex then
metatable.__newindex(self, k, v)
return
end
rawset(meat, k, v)
end
s_metatable.__eq = function(t1, t2)
if metatable.__eq then
return metatable.__eq(self, t2)
end
return rawequal(t1, t2)
end
s_metatable.__len = function(_)
if metatable.__len then
return metatable.__len(self)
end
return #meat
end
--Connect all
s_metatable.__call = wrapMm("call")
s_metatable.__concat = wrapMm("concat")
s_metatable.__umn = wrapMm("umn")
s_metatable.__add = wrapMm("add")
s_metatable.__sub = wrapMm("sub")
s_metatable.__mul = wrapMm("mul")
s_metatable.__div = wrapMm("div")
s_metatable.__mod = wrapMm("mod")
s_metatable.__pow = wrapMm("pow")
s_metatable.__lt = wrapMm("lt")
s_metatable.__le = wrapMm("le")
s_metatable.__tostring = wrapMm("tostring")
s_metatable.__metatable = metatable.__metatable or className
return skin, s_metatable
end
--allow printing of a FakeTable
function ftable.print(...)
local tuple = {...}
for index, item in pairs(tuple) do
if type(item) == "userdata" and getmetatable(item) == className then
local meat, _ = extract(item)
if meat then
tuple[index] = meat
end
end
end
print(table.unpack(tuple))
end
return ftable