Metachain
By @avodey (my main account)!
Get it here! Module
Summary (v1.0.0)
So you use object oriented programming (OOP)? You may use some metatable magic to create basic inheritance:
local Super = {}
local Object = {}
Object.__index = Object
function Super.new()
local self = {}
self.Coins = 0
return setmetatable(self, Object)
end
function Object:Run()
print("Ran", self.Coins)
end
local test = Super.new()
test:Run() --> Ran 0
Super.new()
creates an object that, when you index it and comes out as nil, it asks Object
for the information instead. You can see that the Run
method is called this way.
But what if you wanted to inherit from multiple libraries and/or objects? That’s what this module is for! It will allow you to have a table inherit a library, or another object. You’ll also learn that it’s possible to make specific methods private, which makes them inaccessible through the chain. All the API will be explained later, but for now, let me go over some use cases.
Use Cases
1: Library Extension (Inheritance)
This use case will let you add more methods to your objects.
View Examples
-- module 1:
local Metachain = require(game.ReplicatedStorage.Modules.Metachain)
local Super = {}
local Wrapper = {}
Wrapper.__index = Wrapper
local Object = {}
Object.__index = Object
--\\ Public Methods
function Super.new()
local self = Metachain.new()
self.Component = require(module2).new()
self.Cache = {}
self:AddLibrary(Wrapper)
self:AddLibrary(self.Component)
return self
end
--\\ Wrapper Methods
function Wrapper:Start()
print("Started")
end
function Wrapper:End()
print("Ended")
end
-- module 2:
local Super = {}
local Object = {}
Object.__index = Object
function Super.new()
local self = setmetatable({}, Object)
self.Object = Instance.new("Frame")
return self
end
--\\ Instance Methods
function Object:Update(dt)
print("Updated")
end
This code is a bit long, so let me explain. Objects created by Module 1 inherit an object created by Module 2. If you try calling module 2’s methods, it will work! Let’s test it:
-- module 1:
local test = Super.new()
test:Start() --> Started
test:Update(0) --> Updated
test:End() --> Ended
It works! This use case was primarily the reason I created the module. However, I also made it for the next use case, which allows you to inherit other objects.
2: Object Extension (Polymorphism)
This use case will let you derive an object from another.
View Examples
local Metachain = require(game.ReplicatedStorage.Modules.Metachain)
local Data = {
Coins = 10,
Gems = 10,
}
local Inventory = {
Units = {
{Name = "Solider"}
},
Items = {
{Name = "Star of Nobility"},
},
}
local UserData = Metachain.new(Data)
print(UserData.Units) --> nil
UserData:AddObject(Inventory)
print(UserData.Units) --> {{Name = "Solider"}}
print(UserData.Items) --> {{Name = "Star of Nobility"}}
The code above demonstrates how you can inherit another object. Let me explain the code.
1: Require the Metachain module
3: Get the user’s currency somehow
8: Get the user’s inventory somehow
18: Make a chain object using the user’s data
20: Print the user’s units through the chain, which prints nil
22: Add the Inventory object to the chain
24: Print the user’s units through the chain, which prints a table
25: Print the user’s items through the chain, which prints a table
As you can see, after the AddObject
method is called, you can now access Inventory
through UserData
! An important distiction needs to be made regarding AddObject
and AddLibrary
Chain:AddObject(any)
Allows accessing the object from the chain.
self
is the accessed object.
Chain:AddLibrary(any)
Allows using the library in the chain.
self
is the chain.
AddLibrary
is a substitute for using setmetatable({}, Object)
. Calling methods using a :
will pass self as the chain object. AddObject
has self as the added object itself, and not the chain! Libraries must be tables, while objects can be tables and instances.
3: Private fields, readonly, and setonly
This use case will allow you to set private variables and methods.
View Examples
local Metachain = require(game.ReplicatedStorage.Modules.Metachain)
local Super = {}
local Button = {}
Button.__index = Button
function Super.new()
local self = Metachain.new()
local obj = Instance.new("TextButton")
self:AddLibrary(Button)
self:AddObject(obj)
return self
end
--\\ Instance Methods
function Button:OnClick()
return self.GuiState == Enum.GuiState.Pressed
end
This code has a constructor which creates a table that inherits a text button. Assume that this code is going to be used for an immediate mode environment. Take this explaination with a grain of salt, but immedimate mode means that we will create and change the object every heartbeat. It doesn’t actually recreate it every frame, it’s cached with an id based on the order of objects created.
We don’t want to allow the developer to connect to events because this won’t work with that framework! Currently, you can. We can use :FilterType
to filter a type of return value.
function Super.new()
local self = Metachain.new()
local obj = Instance.new("TextButton")
self:AddLibrary(Button)
self:AddObject(obj)
self:FilterType("RBXScriptSignal")
return self
end
Now any access to RBXScriptSignals will error!
local button = Super.new()
button.MouseButton1Click:Connect(function() --> error: 'RBXScriptSignal' is a private type
end)
Heads up!
This cannot filter nested values. For example, if you have the button instance available, you will be able to access an event from there, which is unideal.
You can also filter particular indexes. If you wanted to lock the ability to change the parent, you could use this code.
self:FilterIndex("Parent")
Changing or getting this value will now error. What if you wanted to make an index or type readonly or setonly! You can, with the second argument!
self:FilterIndex("Parent", "Get")
-- or:
self:FilterIndex("Parent", "Set")
This also works with :FilterType
. If you use "Get"
, then setting will cause an error. If you use "Set"
, then getting will cause an error.
Setonly values are used in RemoteFunctions, where you can set the OnServerInvoke
function. MarketplaceService also has a ProcessRecipet
method which is setonly.
Readonly is used in a ton of places, but you’re probably familar with it’s use in datatypes such as UDim2
, Vector3
, Color3
, etc. They’re readonly because these types are immutable, meaning they cannot be changed. You need to make a new one to seemingly to change it.
API
Super.new(value: T): T & Chain
Creates a new metachain object. The value is returned is not the same table with a metatable attached. Instead, it’s a new table that points to it!
Chain:AddObject(value: table|Instance, only: “Get”|“Set”?): T & Chain
Adds a new object reference to the chain.
only:("Get"|"Set")
Only allows for getting, or setting values.
only:(nil)
Allows for both setting and getting values.
Chain:AddLibrary(value: table): T & Chain
Adds a new library reference to the chain.
Chain:AddWith(value: table|Instance, only: “Get”|“Set”?): T & Chain
Adds a new reference to the chain. Automatically decides whether it is a library or object.
only:("Get"|"Set")
If determined as an object, only allows for getting, or setting values.
only:(nil)
Allows for both setting and getting values.
Chain:FilterIndex(i: string): T & Chain
Errors when this index is asked for in any reference, including the original table.
Chain:FilterType(type: string): T & Chain
Errors when this type is received from any reference, including the original table.
Chain:StopChain(): T & Chain
Removes all the chain methods to remove interference.
Conclusion
Find any bugs? Reply with a script that reproduces the problem! Any context, such as what you are trying to do, would help greatly!