Is there a better workaround to this bit of code with meta-tables? (Recursive indexes), and some other questions

Hello everyone! So, here I’ve written a Vec3 metatable class, just for myself.

Source Code
local Vec2 = require(script.Parent.Vec2)
-- Just a Vec2 class, like this current class. It is needed for Vec3-to-Vec2 conversion.

local Vec3 = {__type = "Vec3"}
local mt = {__index = Vec3}
-- Is |__index = Vec3| actually necessary here? Please explain why and what it actually does.

mt.__add = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a + b.x, a + b.y, a + b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x + b, a.y + b, a.z + b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x + b.x, a.y + b.y, a.z + b.z)
	end
end
mt.__sub = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a - b.x, a - b.y, a - b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x - b, a.y - b, a.z - b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x - b.x, a.y - b.y, a.z - b.z)
	end
end
mt.__mul = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a * b.x, a * b.y, a * b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x * b, a.y * b, a.z * b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x * b.x, a.y * b.y, a.z * b.z)
	end
end
mt.__div = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a / b.x, a / b.y, a / b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x / b, a.y / b, a.z / b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x / b.x, a.y / b.y, a.z / b.z)
	end
end
mt.__mod = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a % b.x, a % b.y, a % b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x % b, a.y % b, a.z % b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x % b.x, a.y % b.y, a.z % b.z)
	end
end
mt.__pow = function(a, b)
	if (typeof(a) == "number") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a ^ b.x, a ^ b.y, a ^ b.z)
	elseif a.__type and (a.__type == "Vec3") and (typeof(b) == "number") then
		return Vec3.new(a.x ^ b, a.y ^ b, a.z ^ b)
	elseif a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return Vec3.new(a.x ^ b.x, a.y ^ b.y, a.z ^ b.z)
	end
end
mt.__eq = function(a, b)
	if a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return (a.x == b.x) and (a.y == b.y) and (a.z == b.z)
	end
end
mt.__lt = function(a, b)
	if a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return (a.x < b.x) and (a.y < b.y) and (a.z < b.z)
	end
end
mt.__le = function(a, b)
	if a.__type and (a.__type == "Vec3") and b.__type and (b.__type == "Vec3") then
		return (a.x <= b.x) and (a.y <= b.y) and (a.z <= b.z)
	end
end
mt.__tostring = function(a)
	return a.x .. " " .. a.y .. " " .. a.z
end
mt.__unm = function(a)
	return Vec3.new(-a.x, -a.y, -a.z)
end
mt.__index = function(t, i)
	--[[
	 This is what I'm talking about: Recursive indexes
	 Like say, you want to index Vec3 with xxy to get values of x, x, and y, in order,
	 as a new Vec3, and then again you might want to index Vec3 with xyx, and so on.
	]]
	-- to Vec3 conversion (modify order)
	if i == "xxx" then return Vec3.new(t.x, t.x, t.x) end
	if i == "xxy" then return Vec3.new(t.x, t.x, t.y) end
	if i == "xxz" then return Vec3.new(t.x, t.x, t.z) end
	if i == "xyx" then return Vec3.new(t.x, t.y, t.x) end
	if i == "xyy" then return Vec3.new(t.x, t.y, t.y) end
	if i == "xyz" then return Vec3.new(t.x, t.y, t.z) end
	if i == "xzx" then return Vec3.new(t.x, t.z, t.x) end
	if i == "xzy" then return Vec3.new(t.x, t.z, t.y) end
	if i == "xzz" then return Vec3.new(t.x, t.z, t.z) end
	
	if i == "yxx" then return Vec3.new(t.y, t.x, t.x) end
	if i == "yxy" then return Vec3.new(t.y, t.x, t.y) end
	if i == "yxz" then return Vec3.new(t.y, t.x, t.z) end
	if i == "yyx" then return Vec3.new(t.y, t.y, t.x) end
	if i == "yyy" then return Vec3.new(t.y, t.y, t.y) end
	if i == "yyz" then return Vec3.new(t.y, t.y, t.z) end
	if i == "yzx" then return Vec3.new(t.y, t.z, t.x) end
	if i == "yzy" then return Vec3.new(t.y, t.z, t.y) end
	if i == "yzz" then return Vec3.new(t.y, t.z, t.z) end

	if i == "zxx" then return Vec3.new(t.z, t.x, t.x) end
	if i == "zxy" then return Vec3.new(t.z, t.x, t.y) end
	if i == "zxz" then return Vec3.new(t.z, t.x, t.z) end
	if i == "zyx" then return Vec3.new(t.z, t.y, t.x) end
	if i == "zyy" then return Vec3.new(t.z, t.y, t.y) end
	if i == "zyz" then return Vec3.new(t.z, t.y, t.z) end
	if i == "zzx" then return Vec3.new(t.z, t.z, t.x) end
	if i == "zzy" then return Vec3.new(t.z, t.z, t.y) end
	if i == "zzz" then return Vec3.new(t.z, t.z, t.z) end
	
	-- to Vec2 conversion
	if i == "xx" then return Vec2.new(t.x, t.x) end
	if i == "xy" then return Vec2.new(t.x, t.y) end
	if i == "xz" then return Vec2.new(t.x, t.z) end
	if i == "yx" then return Vec2.new(t.y, t.x) end
	if i == "yy" then return Vec2.new(t.y, t.y) end
	if i == "yz" then return Vec2.new(t.y, t.z) end
	if i == "zx" then return Vec2.new(t.z, t.x) end
	if i == "zy" then return Vec2.new(t.z, t.y) end
	if i == "zz" then return Vec2.new(t.z, t.z) end
end

function Vec3.new(x, y, z)
	Vec3.x = x or 0
	Vec3.y = y or 0
	Vec3.z = z or 0
	
	return setmetatable(
		Vec3,
		mt
	)
end

function Vec3:Dot(a, b) -- This doesn't work apparently, it may be the script doing something wrong, OR the algorithm is incorrect.
	return (a.x * b.x) + (a.y * b.y) + (a.z * b.z)
end

return Vec3

However, I felt like the code was garbage due to the fact that it has a repeating if “ladder” (repeating “if” per line) at least something like Vec3.xyz = something something, would be nicer than millions of if statements.
Now, Is there any way to improve this? I have a feeling there is, but I also have a feeling that there isn’t. Please let me know.

mt.__index = function(t, i)
	--[[
	 This is what I'm talking about: Recursive indexes
	 Like say, you want to index Vec3 with xxy to get values of x, x, and y, in order,
	 as a new Vec3, and then again you might want to index Vec3 with xyx, and so on.
	]]
	-- to Vec3 conversion (modify order)
	if i == "xxx" then return Vec3.new(t.x, t.x, t.x) end
	if i == "xxy" then return Vec3.new(t.x, t.x, t.y) end
	if i == "xxz" then return Vec3.new(t.x, t.x, t.z) end
	if i == "xyx" then return Vec3.new(t.x, t.y, t.x) end
	if i == "xyy" then return Vec3.new(t.x, t.y, t.y) end
	if i == "xyz" then return Vec3.new(t.x, t.y, t.z) end -- haha i could do .xyz.xyz.xyz.xyz all the time, useless. :D
	if i == "xzx" then return Vec3.new(t.x, t.z, t.x) end
	if i == "xzy" then return Vec3.new(t.x, t.z, t.y) end
	if i == "xzz" then return Vec3.new(t.x, t.z, t.z) end
	
	if i == "yxx" then return Vec3.new(t.y, t.x, t.x) end
	if i == "yxy" then return Vec3.new(t.y, t.x, t.y) end
	if i == "yxz" then return Vec3.new(t.y, t.x, t.z) end
	if i == "yyx" then return Vec3.new(t.y, t.y, t.x) end
	if i == "yyy" then return Vec3.new(t.y, t.y, t.y) end
	if i == "yyz" then return Vec3.new(t.y, t.y, t.z) end
	if i == "yzx" then return Vec3.new(t.y, t.z, t.x) end
	if i == "yzy" then return Vec3.new(t.y, t.z, t.y) end
	if i == "yzz" then return Vec3.new(t.y, t.z, t.z) end

	if i == "zxx" then return Vec3.new(t.z, t.x, t.x) end
	if i == "zxy" then return Vec3.new(t.z, t.x, t.y) end
	if i == "zxz" then return Vec3.new(t.z, t.x, t.z) end
	if i == "zyx" then return Vec3.new(t.z, t.y, t.x) end
	if i == "zyy" then return Vec3.new(t.z, t.y, t.y) end
	if i == "zyz" then return Vec3.new(t.z, t.y, t.z) end
	if i == "zzx" then return Vec3.new(t.z, t.z, t.x) end
	if i == "zzy" then return Vec3.new(t.z, t.z, t.y) end
	if i == "zzz" then return Vec3.new(t.z, t.z, t.z) end
	
	-- to Vec2 conversion
	if i == "xx" then return Vec2.new(t.x, t.x) end
	if i == "xy" then return Vec2.new(t.x, t.y) end
	if i == "xz" then return Vec2.new(t.x, t.z) end
	if i == "yx" then return Vec2.new(t.y, t.x) end
	if i == "yy" then return Vec2.new(t.y, t.y) end
	if i == "yz" then return Vec2.new(t.y, t.z) end
	if i == "zx" then return Vec2.new(t.z, t.x) end
	if i == "zy" then return Vec2.new(t.z, t.y) end
	if i == "zz" then return Vec2.new(t.z, t.z) end

	-- to number conversion already exists, x, y, and z.
end

I have a couple other questions as well, and well, these are:

  • Why is __index = table in the first few lines necessary and what does it do?
    My guess is that it allows code to index in the primary Vec3 table, albeit, I’m not sure.

  • How would I go about making custom functions, and variables such as Vec3:Dot, Vec3:Cross, Vec3.Unit (Constant), Vec3.Magnitude (Constant), etc.?

  • Are there other arithmetic meta-methods?

-fronthd

t[“y”] is another way of saying t.y, and string.sub will allow us to select a particular letter. We can do this.
return Vec3.new( t[i:sub(1,1)], t[i:sub(2,2)], t[i:sub(3,3)] )
To determine Vector3 vs Vector2, just use #i to determine how many letters there are.

__index allows you to specify what happens if ‘i’ doesn’t exist in the table ‘t’.
I suggest you look up a list of all metamethods, there are quite a few.

If you want to make a custom function, add it to ‘t’, or in other words add it to whatever table ‘mt’ is assigned to.
So if you see setmetatable(TabExample, mt) or getmetatable(TabExample), then do this.

TabExample.Magnitude = function(self, whateverargs)
    return (self.x^2+self.y^2+self.z^2)^.5
end

Edit again, sorry I didn’t see the full source.
If you set __index to a table, then when ‘i’ is not found in ‘t’, it will look in the __index for ‘i’. In the case of setting the __index to table the metatable is assigned to, it doesn’t do anything practical.

1 Like

That’s helpful! Is there any way to improve the indexing section, of the code, though? :thinking:

I would like to still have the recursive feature, where you can do .xyz forever.
I just have a feeling there’s a better workaround to this __index function.

I edited my reply to keep everything together and because I missed some things.

1 Like
  1. It’s not necessary but it’s to get the index of that table (if it’s a table) if table’s raw value is nil. In fact you have multiple __index value when you only needed one.
    You can do something like this
mt.__index = function(t, i)
	--[[
	 This is what I'm talking about: Recursive indexes
	 Like say, you want to index Vec3 with xxy to get values of x, x, and y, in order,
	 as a new Vec3, and then again you might want to index Vec3 with xyx, and so on.
	]]
	-- to Vec3 conversion (modify order)
	if i == "xxx" then return Vec3.new(t.x, t.x, t.x) end
	if i == "xxy" then return Vec3.new(t.x, t.x, t.y) end
	if i == "xxz" then return Vec3.new(t.x, t.x, t.z) end
	if i == "xyx" then return Vec3.new(t.x, t.y, t.x) end
	if i == "xyy" then return Vec3.new(t.x, t.y, t.y) end
	if i == "xyz" then return Vec3.new(t.x, t.y, t.z) end -- haha i could do .xyz.xyz.xyz.xyz all the time, useless. :D
	if i == "xzx" then return Vec3.new(t.x, t.z, t.x) end
	if i == "xzy" then return Vec3.new(t.x, t.z, t.y) end
	if i == "xzz" then return Vec3.new(t.x, t.z, t.z) end
	
	if i == "yxx" then return Vec3.new(t.y, t.x, t.x) end
	if i == "yxy" then return Vec3.new(t.y, t.x, t.y) end
	if i == "yxz" then return Vec3.new(t.y, t.x, t.z) end
	if i == "yyx" then return Vec3.new(t.y, t.y, t.x) end
	if i == "yyy" then return Vec3.new(t.y, t.y, t.y) end
	if i == "yyz" then return Vec3.new(t.y, t.y, t.z) end
	if i == "yzx" then return Vec3.new(t.y, t.z, t.x) end
	if i == "yzy" then return Vec3.new(t.y, t.z, t.y) end
	if i == "yzz" then return Vec3.new(t.y, t.z, t.z) end

	if i == "zxx" then return Vec3.new(t.z, t.x, t.x) end
	if i == "zxy" then return Vec3.new(t.z, t.x, t.y) end
	if i == "zxz" then return Vec3.new(t.z, t.x, t.z) end
	if i == "zyx" then return Vec3.new(t.z, t.y, t.x) end
	if i == "zyy" then return Vec3.new(t.z, t.y, t.y) end
	if i == "zyz" then return Vec3.new(t.z, t.y, t.z) end
	if i == "zzx" then return Vec3.new(t.z, t.z, t.x) end
	if i == "zzy" then return Vec3.new(t.z, t.z, t.y) end
	if i == "zzz" then return Vec3.new(t.z, t.z, t.z) end
	
	-- to Vec2 conversion
	if i == "xx" then return Vec2.new(t.x, t.x) end
	if i == "xy" then return Vec2.new(t.x, t.y) end
	if i == "xz" then return Vec2.new(t.x, t.z) end
	if i == "yx" then return Vec2.new(t.y, t.x) end
	if i == "yy" then return Vec2.new(t.y, t.y) end
	if i == "yz" then return Vec2.new(t.y, t.z) end
	if i == "zx" then return Vec2.new(t.z, t.x) end
	if i == "zy" then return Vec2.new(t.z, t.y) end
	if i == "zz" then return Vec2.new(t.z, t.z) end

	-- to number conversion already exists, x, y, and z.
        return Vec3[i]
end
  1. It’s simple. Add it to the table named Vec3 field (assuming __index == Vec3) and ensure it’s reciveded when indexing the value.
  2. Lua 5.3 has __idiv metamethod along with __shl (left shift), __shr (right shift), __band (bitwise and), __bor (bitwise or), __bnot (bitwise not) and __bxor (bitwise xor).
1 Like
local mag = something.Magnitude()

The example you provided is a function, not a variable that gets calculated once the Vec3 has been created. I would like to recreate the Vector3 class in the Roblox api.

How would I know x, y, and z once the Vec3 has been created? Like how would the table named Vec3 know the values of x, y, and z? I need these to add a magnitude to the Vec3 field, or… any other workaround to this?

Sorry for my terrible understanding, it’s my first time I really understand metatables, and metamethods. I still haven’t got full understanding of it, yet. :man_facepalming:

Does this work?

function Vec3.new(x, y, z)
	Vec3.x = x or 0
	Vec3.y = y or 0
	Vec3.z = z or 0
	Vec3.magnitude = ((Vec3.x ^ 2) + (Vec3.y ^ 2) + (Vec3.z ^ 2)) ^ 0.5
	
	return setmetatable(
		Vec3,
		mt
	)
end

I suppose that’s true. I don’t want to retype it all, but here is an example I gave someone in a private message. Basically, you need to create a table full of the methods and static/default properties of Vec3 like you currently have, but change the Vec3.new function so that rather than creating a new metatable from Vec3, it acts as a constructor for a new userdata (using newproxy) or a new table. Feel free to send me a direct message with any questions about it, and if I have time I might even write you a more specific example.
This rest of this post is from the message.

It is important to know that this is not tested, and that I haven’t done scripting for a couple of years. This should be a pretty spot-on example though, and it has practical functionality assuming it works.
Basically, we create a new class to give us enhanced functionality for our players, to enable easier coding.

local DSSaveData = game:GetService("DataStoreService"):GetDataStore("SaveData")
local ObjPlayer = {} -- Class table
local PlayersList = {} -- A place to store our list of players. This is not required for all OOP, but is required for some of the functionality we are adding.
do -- Create a new block for the sake of cleanliness, i.e. we can use the arrows on the left to minimize this whole class definition.

	ObjPlayer.Add = function( self, player ) -- Self is automatically provided when we call ObjPlayer:Add(p) using the colon as such. To be used as ObjPlayer:Add(PlayerWhoJoined)
		local Object = newproxy( true ) -- Create a new userdata and add it to PlayersList, also a setup line or two
		PlayersList[player.UserId] = Object
		local MT = getmetatable(Object)

		local Vars = { -- A list of standard variables to give this object when it is created
			["Name"] = player.Name,
			["CustomName"] = player.Name,
			["UserId"] = player.UserId,
			["JoinTime"] = tick(),
			["__mt"] = MT -- In case we need the metatable later and externally
		}

		Vars.Data = {
			["Kills"] = DSSaveData:GetAsync("Kills_"..Player.UserId) or 0, -- Data is saved in plr.Data, here we retrieve saved data or use the defaults.
			["Deaths"] = DSSaveData:GetAsync("Deaths_"..Player.UserId) or 0
		}

		local CustomVars = {} -- This is an optional thing here, but basically if someone tries to assign a variable that doesn't exist, I will let them by using this table. You will see how later.

		local NewIndexFunctions = { -- When a value is being assigned to this object, these functions will deal with it.
			["CustomName"] = function( self, value )
				self.Vars.CustomName = value
				ChangeUsernameBlahBlah(CustomName) -- Call an outside function that will update GUIs and such to the new name. I'm not here to make that function for you lol, this is an example class.
			end,
		}

		MT.__newindex = function( self, key, value )
			if NewIndexFunctions[key] then
				NewIndexFunctions[key](self, value) -- If key matches a NewIndex function, call the function and let it handle the behavior.
			else
				CustomVars[key] = value -- Remember, CustomVars is not a table that has been assigned a metatable. __newindex will _always_ fire, even if it already exists.
			end
		end

		MT.__index = function( self, key )
			return Vars[key] or ObjPlayer[key] or CustomVars[key] -- This line is a quicker way of saying "check Vars, then check ObjPlayer, then check CustomVars if nothing is found yet"
			-- Returns what it finds in Vars, or what it finds in ObjPlayer, or what it finds in CustomVars, or nil.
			-- ObjPlayer is where we keep the methods and functions related to this class, so it is important to include it.
			-- We can also create the functions inside of ObjPlayer.Add function instead of just ObjPlayer, but this increases memory usage.
		end

		MT.__len = function( self ) -- Since you wanted some ideas for the __len operator, if you use #SomePlayer it will return how long they've been in the server.
			return tick()-self.JoinTime -- 'self' refers to the userdata, which has a __index referenceing the Vars table. I could speed this up by using Vars.JoinTime instead of self.JoinTime, but that wouldn't be a great example.
		end
		return Object
	end -- End of the ObjPlayer.Add function.


	ObjPlayer.Remove = function ( self ) -- This is where data is saved, any you can also call any external functions that should run when a player leaves.
		DSSaveData:SetAsync("Kills_"..self.UserId, self.Data.Kills) -- You are technically more at risk for losing save data by using SetAsync instead of UpdateAsync, but again that isn't the point here.
		DSSaveData:SetAsync("Deaths_"..self.UserId, self.Data.Deaths)
		print(self.Name .. " has left the game")
		PlayersList[self.UserID] = nil
		self.__mt.__mode = "v" -- I am not well versed in this, but theoretically this is the line you need to prevent memory leaks. It adds __mode = "v" to the metatable, which allows it to be garbage collected. I do not know if it works quite this way.
	end

	local MT = {} -- As a bonus, I'm making ObjPlayer a metatable too!
	MT.__len = function( self )
		return #PlayersList -- If you use #ObJPlayer, it will tell you how many players are in the game!
	end
	setmetatable(ObjPlayer, MT)
end


game.Players.PlayerAdded:Connect(function( player )
	local p = ObjPlayer:Add(player) 
	p.Team = math.random()<.5 and "Red" or "Blue" -- You remember the CustomVars table? This is how it is used. Say you put this class definition in a ModuleScript and re-use it everwhere, you can basically still add more functionality through this method.
	-- In this instance, I added teams which previously had no functionality, and people are assigned a random team. This is by no means a balanced team system, but for the last time I am only doing this by way of example.
end)

game.Players.PlayerRemoving:Connect(function( player )
	PlayersList[player.PlayerId]:Remove()
end)

Later on, we could add functions like ObjPlayer:Ban(Time) to create an automatic timed ban system, or a function that automatically shuffles the teams. OOP is powerful after all. I have one more suggestion.

If you like OOP, look into MoonScript . It is an OOP language that is easier to write than Lua. It compiles into messy Lua garbage, but so long as you keep your MoonScript source code separate, you will be fine. If you give MoonScript a try but have trouble, let me know!

That does not work, but only because it is using Vec3. You basically need to copy the table, or better yet use __index to specify Vec3 as the table containing all of the methods. What you have there is a single object, and if you were to call Vec3.new twice, it would screw things up.