Inheriting Instance Methods

Hello!

I am wondering if it is possible for a custom object I created to inherit the methods of a Roblox instance. For example, if I make a class called “EnhancedPart”, which is essentially a normal Roblox part with some additional functionality, is it possible for me to call part’s :GetMass() method on the instantiated version of “EnhancedPart”?

Before you all yell at me, yes, I am aware that the number of apparent use cases of such a design are quite limited. I am also aware that there are a number of workarounds that work completely fine, such as assigning the instance to its own field in the object or creating homebrewed methods that implement the instance’s methods within the object, but I want to know if it’s possible to achieve exactly what I’ve stated above. This is more out of curiosity than anything else.

I have been able to create a design that allows me to read the fields/properties of an instance. Here it is so far:

--ModuleScript (EnhancedPart class)
local ePart = {}

function ePart.new()
	local self = setmetatable({}, ePart) --Sets self's metatable to ePart class
	ePart.__index = ePart --Look through ePart if no such field/method exists in self
	
	local part = Instance.new("Part", workspace) --New part!
	
	local secondTable = {} --A metatable for ePart
	local partTable = setmetatable(ePart, secondTable) --Set it
	secondTable.__index = part --Look through part's data if no such field/method exists
	
	return self
end

return ePart

--Script (just used for testing of EnhancedPart)
local ePart = require(script.Parent.EnhancedPart)
local p1 = ePart.new()
print(p1.Transparency) --prints 0
print(p1:GetMass()) --Throws an error

Here is the error I’m receiving:

As you can see, p1.Transparency prints out fine. Calling the method, however, yields an error. I don’t understand why it assumes I’m using a ‘.’ instead of a “:”. My guess is that something about my assigning secondTable.__index to a part is throwing everything off.

Another potential solution that I have stumbled across while researching is to retrieve a direct table of the instance’s properties and methods via HTTP requests, but I really don’t want to do that. I want to know if there is a plausible method to inherit all of an instance’s properties via metatable/metamethod magic.

Again, I understand that this seems intuitively useless because there are a number of workarounds that work just fine, such as assigning the object’s field to the instance. There is something just so clean about the thought of being able to call my custom object with inherited methods from an instance.

1 Like

Sorry, I don’t really understand what you’re trying to make. However, from the error you got, I can say this:

-- Calling methods with '.'
workspace.ClearAllChildren() -- Error: Expected ':' not '.'
workspace.ClearAllChildren(workspace) -- This works with no error!

Maybe this information can help?

Is there a specific part that I am not explaining well?

I am essentially trying to create a custom subclass of the Part instance by directly inheriting its fields and methods through metatables, in the same way that Part is a subclass of BasePart. If you check the documentation for Part, you’ll notice that Part inherits fields and methods from BasePart, PVInstance, and Instance. I’m trying to make an extension of Part that has more specified functionality (EnhancedPart). EnhancedPart’s superclass would be Part (and its superclass is BasePart). I’m just trying to extend the chain in a custom manner. Obviously, this is not completely possible because we cannot make our own instances, but I can still make an object that replicates such functionality. This is all premised on the idea of object-oriented programming. If you’re not familiar with it, here is a link explaining how it works in Lua.

Also to your code, that syntax is very interesting. I did not know Lua had two ways to call object methods. Thanks for that :slight_smile:

It has something to do with self. You may know this but, just incase:

function module:Example()
    print(self) -- Self can exist here
end

function module.Example(self)
    print(self) -- This function is the exact same as above
end

function module.Example()
    print(self) -- nil
end

You see with the colon, the self argument is not needed. With the dot, you need to specify self. Otherwise, you’re just indexing the metatable and it doesn’t know what object you want to modify.

Also I understand what you’re trying to do a little more now.

I made a custom wrapper for something like this a while back:

The code is pretty messy, so I recommend changing and modifying it to fit your specific needs.

Yeah, I am aware of differences between the two syntaxes. I was just not aware that the .MethodName(self) syntax also worked for built-in Roblox methods.

I highly suggest you read more about the object-oriented programming paradigm. You’ll get to see the true power of the colon and dot operators! While I would argue that Lua is not an inherently object-oriented programming language (this claim is heavily debatable), you can still replicate object-oriented functionalities fairly well in Lua.

Here are some resources that will explain OOP:
What is object-oriented programming? OOP explained in depth
All about Object Oriented Programming - this one is tailored for Roblox

Thanks! I’ll check it out when I can.

Well, roblox instances are basically just tables with metatables, so yes it would also work with them.

Changing that code to this might work:

secondTable.__index = function(_, index)
	local val = 
	if typeof(val) == "function" then
		return function(...)
			return val(part, ...)
		end
	else
		return val
	end
end

you can inherit instance methods and properties by using metatables

--assuming your code is instancing a part and storing it you can do this
metatable.__index = function(self, index)
   local part = self.part
   return part[index]
end

and this would work ok I guess, but notice how the part things don’t actually show up in the autocomplete, and this is for the very simple reason of roblox doesn’t know what you want to do!

however you can fix this by the power of typing!

In the roblox type compiler you can combine two types by using the “union operator” (&)

export type customObject = {
  property:number;
} & BasePart
--custom type "inheriting" from BasePart

sometimes though you want the return type of the constructor to be auto-typed! and we can do this!

function tbl.new()
  local self = setmetatable({}, meta)

  --Unfortunately you won't get typed autocomplete inheriting from part here
  --that would result in the "self" table being locked and no changes will be detected

  --:: is the type assertion, essentially just returning self combined with basepart
  return self::typeof(self) & BasePart
end

do note though that we removed the __index of the table! which means we can’t add any methods!

however we can simply just add a new if to yoink properties from the methods list
then its just a matter of retyping metatable to be what you want

local metatable:{
  __index:typeof(BaseClassName)
  --notice __index is set to baseclassname even though it's a function
  --this tricks the type compiler into thinking __index is actually BaseClassName
} = {}

metatable.__index = function(self, index)
   local val = BaseClassName[index]
   if val then return val end

   local part = self.part
   return part[index]
end


function tbl.new()
  local self = setmetatable({}, meta)

  --Unfortunately you won't get typed autocomplete inheriting from part here
  --that would result in the "self" table being locked and no changes will be detected

  --:: is the type assertion, essentially just returning self combined with basepart
  return self::typeof(self) & BasePart
end

or if that’s too complicated for you, you can simply hard type the return

export type FunnyPart = {
   part:BasePart;
   
   MethodExample:(FunnyPart, string)->nil;--method that takes a string and returns nil
} & BasePart

function FunnyPart.new():FunnyPart--<< set return type as the part type you hard-created

end

edit: you would need to call methods like FunnyPartObject.GetMass(FunnyPart.part) unless you create a proper wrapper function like this

function FunnyPart:GetMass()
   return self.part:GetMass()
end

If anybody is interested, I made another attempt at this and I figured it out.

local extender = {}

function extender.instance(t, instance, ...)
    local p = ...
    local mt = {}
    local find
    function find(t, i, instance, ...)
        if instance == nil then
            return
        end
        local success, value = pcall(function()
            return instance[i]
        end)
        if success then
            if type(instance[i]) == "function" then
                return function(t, ...)
                    return instance[i](instance, ...)
                end
            end
            return value
        end
        return find(t, i, ...)
    end
    mt.__index = function(t, i)
        return find(t, i, instance, p)
    end
    setmetatable(t, mt)
end

return extender

instance takes in a wrapper class table and a tuple of instances to inherit the methods of. Because instances have overlapping attributes, the precedence is in the order of how they are submitted in the tuple. For example, if I wanted to make a custom signal, I could do something like this:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Dependencies = ReplicatedStorage.Dependencies
local extender = require(Dependencies.extender)

local CustomEvent = {}

function CustomEvent.new()
	local self: BindableEvent & RBXScriptSignal = {}
	self.Event = Instance.new("BindableEvent")
	extender.instance(self, self.Event.Event, self.Event) --extends the RBX signal and the BindableEvent
	return self
end

return CustomEvent

In this case, the RBX’s signal’s overlapped attributes with BindableEvent’s attributes would take precedence.