Metachain (easy polymorphism and inheritance)!

Metachain


By @avodey (my main account)!

Get it here! :package: 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)

:warning: 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!

:+1:

4 Likes

I wanted to check this out, to see what Chain is all about.

When I tried something like this, I basically did a repetitive thing with the generic types, so that one can call a function and pass multiple classes through.

Usually I am very skeptical over libraries, due to support of the autocomplete or the documentation.

I am already confused at the difference between AddObject and AddLibrary.

 

When I used to experiment with metatables, one of the things I would encounter is a ghost. I call it ghost because it was something where it was hard to tell on what was going on.

Basically, a table uses a memory address and all the classes were using the same memory address, they never created a new table.

 

Also, there’s a typo in “Soilder”, I guess.

 

I am not exactly sure which one is the equivalent of mergeClasses from that other thread.

I think it’s AddLibrary and AddObject.

I guess the difference between Object and Library, is that Library is just what you read (I mean it makes sense) and Object is an already created Object, that also metatable’d from a Library, so metamethods.

 

I’d be the type of guy that would create a Module and put that what AddLibrary and AddObject does into one small little Utility function, just for pure simplicity.

When you showed me this I actually thought it was something for the autocomplete.
image

The autocomplete is a bit tricky here. The first definition you make to a variable is basically used as the absolute.

See
image

A trick would be to re-use local, but I am not really sure how practical that would be for whatever will process the code when the game runs.

I knew I did a pretty bad job explaining what those two are…

Let me start with AddLibrary. This allows you to do what you know and love: inheritance. Instead of doing this:

local Object = {}
Object.__index = Object

-- later:
return setmetatable(self, Object)

You can do this:

local Object = {}
Object.__index = Object

-- later:
local self = Metachain.new()
self:AddLibrary(Object)
return self

It simply allows accessing another library. When calling methods, self is the one created in the object constructor you made. This is true for both of the examples above.


AddObject is similar, but there is one key difference. self is instead the object itself. This allows things like accessing instance data directly from a table!

local self = Metachain.new()

local button = Instance.new("TextButton")
self:AddObject(button)

self.Success = true

return self

-- later:
local test = Super.new()
print(test.Success) --> true
print(test.BackgroundColor3) --> 1, 1, 1

If you’re only using one library, then I wouldn’t recommend using the module. It’s only for if you want to chain an object and/or multiple libraries!


Additionally, the autocomplete is not reliable. You should instead set your own specific type like this:

export type Object = {

} & typeof(Object)

-- later:
local self: Object & Metachain.Chain = Metachain.new()