Metachain (basic inheritance)!

Metachain


By @avodey (my main account)!

:package: Get Module

Summary (v1.1.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.

API


Super.new(table): 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”?): 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): Chain

Adds a new library reference to the chain.

Chain:AddWith(value: table|Instance, only: “Get”|“Set”?): 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:StopChain()

Removes all the chain methods to remove interference.

Conclusion


I’d love to hear your feedback! If you have questions, or find a bug, you should leave a reply!

:+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()

Just updated this module after using it for a while. I removed all the filtering methods, as it was restricting the ability to save these metachain objects to Datastores. The actual table returned when using Super.new is empty, but you would need it to be the original table for it to save!

Sorry for the inconvenience if you needed this functionality.