__index-ing object/instance functions?

The following code searches an object if it cant find the value of the index in the table t. It worked relatively well until I discovered an interesting syntax. If I use the colon operator to call a function on table t, despite it being indexed directly from instance, self will be t instead of the object that it belonged to.

local class = {}
class.__index = class

function class.new()
	return setmetatable({},class)
end

function class.returnText()
	return "function with dot (.) operator"
end

function class:returnText2()
    -- self is actually the table 't', not the actual class object
	return "function with colon (:) operator"
end

local m = {
	__index = function(self,i)
		return rawget(self,"instance")[i]
	end,
	
	__newindex = function(self,i,v)
		rawget(self,"instance")[i] = v
	end,
}

local t = setmetatable({instance = class.new()},m)

This seems to be intentional behavior on Lua’s part, but I don’t prefer it; what I really need this for is to index an instance, as such:

local m = {
	__index = function(self,i)
		return rawget(self,"instance")[i]
	end,
	
	__newindex = function(self,i,v)
		rawget(self,"instance")[i] = v
	end,
}

local t = setmetatable({instance = Instance.new("Part",workspace)},m)

I’m trying to create a dynamic object where it can index the instance while also being able to index it’s own custom members. I’m rewriting one of the bigger systems in my game and I would like to rewrite as little code as possible, which is why this object would be lovely.

Thanks

I’m not fully certain I understand what you want - do you want a table that can be used like this?

local t = setmetatable({instance = ...; CustomMember = true}, m)
-- index instance
print(t.Parent)
t.Name = "Name"
-- index custom members
print(t.CustomMember)
t.NewCustomMember = true

Assuming this is the case, the latter block of code you wrote appears to accomplish what I think you’re describing, at least partially.

The __index metamethod will only be called if the attempted index in the table will be nil, so if you already have custom members in the table then there’s no need for anything fancier - the metamethod only needs to handle the instance case.

For example if you have a table local t = setmetatable({instance = ...; CustomMember = true}, m), indexing will work in the following ways:

  • t.Parent will call the __index metamethod
  • t.CustomMember will return true without calling the __index metamethod, since it is defined in the table

However, having custom members that are allowed to be nil will make this more complicated, since this means the __index metamethod needs to somehow know which indexes refer to custom members and which refer to the instance’s members.

One solution is to define a dictionary of custom members and look up the index in the dictionary to decide what should be indexed.

local CUSTOM_MEMBERS = {
   CustomString = true;
   CustomNumber = true;
}

local m = {
   __index = function(self, i)
      if not CUSTOM_MEMBERS[i] then
         return rawget(self, "instance")[i]
      end
   end
}

Alternatively you could index the instance first, wrapped in a pcall to catch the error. This’ll be comparatively slow, but hey it works and doesn’t require any manual work.

local m = {
   __index = function(self, i)
      local success, result = pcall(function()
         return rawget(self, "instance")[i]
      end)
      if not success then
         result = nil
      end
      return result
   end
}

__newindex faces a similar problem because it is also only called when the value in the table is nil, and it can be solved in the same ways.

Note that the solutions given above assume that custom members are stored inside self, which means that if the method is called and it is not indexing the instance, the result will always be nil.

i.e. assumes t = {instance = ...; CustomMember = true}

On the other hand if you have custom members stored elsewhere, then the solutions will be largely the same, just explicitly indexing that table in the non-instance case in the metamethods.

i.e. t = {instance = ...; members = {CustomMember = true}} and the metamethods will use rawget(self, "members")[i] instead of giving up/returning nil.

If you want something that’s a bit fancier than the solutions I’ve provided, you’re probably better off just explicitly writing code that distinguishes between instance members and custom members.

For example, t:GetProperty(index) and t:GetValue(index) for instance and custom members respectively. It’s a bit annoying, but that’s just how it is. I believe that this way is easier to debug and understand.

I’m talking about the function aspect of it; imagine this scenario:

local m = {
	__index = function(self,i)
		return rawget(self,"instance")[i]
	end,
	
	__newindex = function(self,i,v)
		rawget(self,"instance")[i] = v
	end,
}

local t = setmetatable({instance = Instance.new("Part",workspace)},m)

print(t:GetPivot())

This code is going to error with "Expected ':' not '.' calling member function GetPivot". Instead of calling GetPivot on the instance itself, it’s calling it on t.

Most of this anyways is just to avoid having to use redundant code such as t.instance:GetPivot(), but I understand that this may not be possible. Again, I’m trying to have to rewrite as little as code as possible so I don’t disrupt my workflow.

Thanks for the detailed response, though. I may just be better off directly indexing the instance to call its functions.

1 Like

Yeah for that particular problem, the one solution I can think of is returning from the __index some sort of “method wrapper” table that has its own metatable that implements __call. Also need to manually remove the first argument (which is automatically added by the colon operator)

For example like this:

local methodMeta = {
	__call = function(methodSelf, classSelf, ...)
		return methodSelf.func(classSelf.instance, ...)
	end
}

local classMeta = {
	__index = function(self, i)
		local success, result = pcall(function()
			return rawget(self, "instance")[i]
		end)
		if not success then
			result = nil
		elseif type(result) == "function" then
			result = setmetatable({func = result}, methodMeta)
		end
		return result
	end
}

Usage example:

local t = setmetatable({instance = Instance.new("Part")}, classMeta)
t.instance.Parent = workspace
print(t:GetFullName())
1 Like