[1.0.0] Tuple and Lists

I’ve decided to create two more data types on Lua, tuple and list.
There’s no long-term benefits of using list over table and this is made for fun and just because I was bored, but this can make your workflow easier.

You can download it here (BSD 2-Clause Licence):
1.0.0: TupleAndList.rbxm (5·9 KB)

Tuple

Inspired by Python tuple.

This is a special one, unlike tables, tuples aren’t passed by reference and is immutable. That means it can equal to each other, and once it has been created, it cannot be modified.
No, I’m serious, this is truly immutible, it actually equals to each other, prove it yourself with rawequal.

print(rawequal(Tuple.new(1, 2, 3), Tuple.new(1, 2, 3))) --> true
print(rawequal(Tuple.new(1, 2, 3, 4), Tuple.new(1, 2, 3, 4))) --> true
print(rawequal(Tuple.new('d', 'e', 'f', 'g'), Tuple.new('d', 'e', 'f', 'g')) --> true

Features:

  • Not passed by reference, unlike tables.
  • You can concatinate tuples with the + or the .. syntax.
  • You can can index using tuples
local variable = { [Tuple.new('hello', 'world')] = 'hello!' };
print(variable[Tuple.new('hello', 'world')]); --> hello!

This can be indexed as opposed to Lua’s tuple.
You can access the tuple by module.Tuple keyword.

Lists

This is a inspried by JavaScript’s array and Python’s list.
There are no long-term benefits for using this over a table, this is built for fun, there are no real use for this over table.
The alternative is this:

setmetatable([Insert table here], { __index = table });

Features included that Lua table didn’t have:

  • Copy with the :copy() function
  • Reverse with the :reverse() function
  • Filter (remove all elements that didn’t pass the function determined by truthy and falsey values) with the :filter(function) function
  • Concatinate lists with the + or the .. syntax. (This creates a new list)
  • Can be negative indexed, list[-1] is a sugar syntax for list[#list - 1]

It can be accessed by module.list, and can be created by list.new { }

Note

  • It’s zero based index instead of one, that means getting the first item would require you to do list[0] instead of list[1].
6 Likes

Hi there~ @Blockzez

Thank you for sharing your creation with us, this is very interesting stuff.

Would you kindly create a link to Source code (preferably Github and/or Pastebin) for people who want to read the source but don’t have any access to a PC or maybe want to contribute to the source

I know you can download the file but some people are on Mobile devices.

1 Like

Sure. (584 lines).

--[=[
	Version 1.0.0
	This is intended for Roblox ModuleScripts
	BSD 2-Clause Licence
	Copyright ©, 2020 - Blockzez (devforum.roblox.com/u/Blockzez and github.com/Blockzez)
	All rights reserved.
	
	Redistribution and use in source and binary forms, with or without
	modification, are permitted provided that the following conditions are met:
	
	1. Redistributions of source code must retain the above copyright notice, this
	   list of conditions and the following disclaimer.
	
	2. Redistributions in binary form must reproduce the above copyright notice,
	   this list of conditions and the following disclaimer in the documentation
	   and/or other materials provided with the distribution.
	
	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
	AND ANY EXPRESS OR IMPLIED WlistANTIES, INCLUDING, BUT NOT LIMITED TO, THE
	IMPLIED WlistANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
	DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
	FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
	DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
	SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
	CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
	OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]=]--
local tup = { };
local list = { };
local module = setmetatable({ }, { __index = { tuple = tup, list = list }, __newindex = function() error("Attempt to modify a readonly table", 2) end, __metatable = "The metatable is locked" });

-- A pointer so it won't throw an error for index being nil
local nullpointer = newproxy();

-- A placeholder pointer for getting the data
local getdata = newproxy();

local proxy = { };
local addr = { };
local hashes = { };

--[=[ Tuple ]=]--
local function nullifnil(value)
	if value == nil then
		return nullpointer;
	end;
	return value;
end;

local function compare_list(left, right)
	for i = 0, math.min(#left, #right) - 1 do
		if left[i] ~= right[i] then
			return (left[i] > right[i]) and 1 or -1;
		end;
	end;
	return (#left > #right) and 1 or ((#left < #right) and -1 or 0);
end;

-- Operators
-- <= and =>
local function le(left, right)
	return compare_list(left, right) <= 0;
end;

-- < and >
local function lt(left, right)
	return compare_list(left, right) < 0;
end;

-- == (This is NOT rawequal)
local function eq(left, right)
	if #left ~= #right then
		return false;
	end;
	for i = 0, #left - 1 do
		if left[i] ~= right[i] then
			return false;
		end;
	end;
	return true;
end;

-- + and ..
local function concat_tup(left, right)
	if not (proxy[left] and proxy[right]) then
		error("attempt to concatenate " .. typeof(right) .. " with " .. typeof(right), 2);
	end;
	local ret = { };
	for _, v in ipairs(proxy[left].data) do
		table.insert(ret, v);
	end;
	for _, v in ipairs(proxy[right].data) do
		table.insert(ret, v);
	end;
	return tup.fromList(ret);
end;

-- *
local function mul_tup(left, right)
	if proxy[left] and proxy[right] then
		error("attempt to perform arithmetic (mul) on userdata", 2);
	elseif type(left) ~= "number" and type(right) ~= "number" then
		error("attempt to perform arithmetic (mul) on " .. typeof(left) .. " and " .. typeof(right), 2);
	end;
	local rep, tuple;
	if type(right) == "number" then
		rep = right;
		tuple = proxy[left].data;
	else
		rep = left;
		tuple = proxy[right].data;
	end;
	local ret = { };
	for _ = 1, rep do
		for _, v in ipairs(tuple) do
			table.insert(ret, v);
		end;
	end;
	return tup.fromList(ret);
end;

-- Other
local function len(self)
	return #proxy[self].data;
end;

local function tostr_tup(self)
	return table.concat(proxy[self].data, ', ');
end;

-- Index
local function getattr_tup(self, ind)
	if type(ind) == "number" then
		if ind >= #self or ind < -#self then
			error("tuple index out of range", 2);
		end;
		if ind < 0 then
			return proxy[self].data[(#self + ind) + 1];
		end;
		return proxy[self].data[ind + 1];
	elseif type(ind) ~= "string" then
		return;
	end;
	if ind:match('Item[1-9]%d*') then
		return proxy[self].data[tonumber(ind:sub(5))];
	end;
	return (ind ~= "new" and ind ~= "fromList") and tup[ind];
end;

-- Constuctors
function tup.new(...)
	return tup.fromList { ... };
end;

function tup.fromList(list)
	if type(list) ~= "table" and not proxy[list] then
		error("bad argument #1 (table expected, got" .. typeof(list) .. ")", 2);
	end;
	local position = hashes;
	local list_copy = { };
	if type(list) == "table" then
		local i = 1;
		for ind, val in next, list do
			-- Check
			if ind ~= i then
				error("bad argument #1 (array expected, got dictionary)", 2);
			end;
			
			if not position[nullifnil(val)] then
				position[nullifnil(val)] = { };
			end;
			position = position[nullifnil(val)];
			
			list_copy[ind] = val;
			i = i + 1;
		end;
	elseif proxy[list].list then
		for _, val in list do
			if not position[nullifnil(val)] then
				position[nullifnil(val)] = { };
			end;
			position = position[nullifnil(val)];
			
			table.insert(list_copy, val);
		end;
	else
		return list;
	end;
	
	if position[getdata] then
		return position[getdata];
	end;
	
	local pointer = newproxy(true);
	position[getdata] = pointer;
	proxy[pointer] = { list = false, data = list_copy };
	addr[pointer] = tostring(pointer):sub(11);
	
	local pointer_metatable = getmetatable(pointer);
	pointer_metatable.__metatable = "The metatable is locked";
	pointer_metatable.__index = getattr_tup;
	
	-- Operators
	pointer_metatable.__le = le;
	pointer_metatable.__lt = lt;
	pointer_metatable.__eq = eq;
	pointer_metatable.__add = concat_tup;
	pointer_metatable.__concat = concat_tup;
	pointer_metatable.__mul = mul_tup;
	
	-- Iterator
	local ind = 0;
	pointer_metatable.__call = function()
		ind = ind + 1;
		if ind > #proxy[pointer].data then
			ind = 0;
			return;
		end;
		return ind - 1, proxy[pointer].data[ind];
	end;
	
	-- Others
	pointer_metatable.__len = len;
	pointer_metatable.__tostring = tostr_tup;
	
	return pointer;
end;

-- Methods
setmetatable(tup, { __newindex = function(self, ind, func) rawset(self, ind, 
	function(self1, ...) if (not proxy[self1]) then error(self == module and ("bad argument #1 (tuple expected, got " .. typeof(self) .. ')')
	or ("Expected ':' not '.' calling member function " .. ind), 2); end; return func(self1, ...) end); end; });

function tup:unpack()
	return unpack(proxy[self].data);
end;

function tup:filter(func, self_arg)
	if type(func) ~= "function" then
		error("bad argument #2 (function expected, got " .. typeof(func) .. ')', 2);
	end;
	local ret = { };
	
	for _, val in ipairs(proxy[self].data) do
		if self_arg and func(self, val) or func(val) then
			table.insert(ret, val);
		end;
	end;
	
	return tup.fromList(ret);
end;

function tup:index(val)
	return table.find(proxy[self].data, val);
end;

function tup:count(val, self_arg)
	if self_arg ~= nil then
		if type(val) ~= "function" then
			error("bad argument #2 (function expected, got " .. typeof(val) .. ')', 2);
		end;
	end;
	local ret = 0;
	
	for _, val1 in ipairs(proxy[self].data) do
		if self_arg ~= nil then
			if self_arg and val(self, val1) or val(val1) then
				ret = ret + 1;	
			end;
		elseif val == val1 then
			ret = ret + 1;
		end;
	end;
	
	return ret;
end;

function tup:pop(ind)
	if type(ind) ~= "number" then
		error("bad argument #2 (integer expected, got " .. typeof(ind) .. ')', 2);
	elseif ind % 1 ~= 0 then
		error("bad argument #2 (integer expected, got float)", 2);
	end;
	local ret = { };
	
	ind = ind or -1;
	if ind < 0 then
		ind = #self + ind;
	end;
	for k, v in ipairs(proxy[self].data) do
		if k - 1 ~= ind then
			table.insert(ret, v);
		end;
	end;
	
	return tup.fromList(ret);
end;

--[=[ Lists ]=]--

-- + and ..
local function concat_list(left, right)
	if not (proxy[left] and proxy[right]) then
		error("attempt to concatenate " .. typeof(right) .. " with " .. typeof(right), 2);
	end;
	local ret = { };
	for _, v in ipairs(proxy[left].data) do
		table.insert(ret, v);
	end;
	for _, v in ipairs(proxy[right].data) do
		table.insert(ret, v);
	end;
	return list.new(ret);
end;

-- *
local function mul_list(left, right)
	if proxy[left] and proxy[right] then
		error("attempt to perform arithmetic (mul) on userdata", 2);
	elseif type(left) ~= "number" and type(right) ~= "number" then
		error("attempt to perform arithmetic (mul) on " .. typeof(left) .. " and " .. typeof(right), 2);
	end;
	local _list = list;
	local rep, list;
	if type(right) == "number" then
		rep = right;
		list = proxy[left].data;
	else
		rep = left;
		list = proxy[right].data;
	end;
	local ret = { };
	for _ = 1, rep do
		for _, v in ipairs(list) do
			table.insert(ret, v);
		end;
	end;
	return _list.new(ret);
end;

-- Other
function tostr_list(self)
	return "list: " .. addr[self];
end;

-- Index
local function getattr_list(self, ind)
	if type(ind) == "number" then
		if ind >= #self or ind < -#self then
			error("list index out of range", 2);
		end;
		if ind < 0 then
			return proxy[self].data[(#self + ind) + 1];
		end;
		return proxy[self].data[ind + 1];
	elseif type(ind) ~= "string" then
		return;
	end;
	return (ind ~= "new" and ind ~= "pack") and list[ind];
	
end;

local function setattr_list(self, ind, val)
	if type(ind) ~= "number" then
		if type(ind) ~= "string" then
			error("attempt to index userdata with '" .. typeof(ind) .. "'", 2);
		end;
		error("attempt to index userdata with '" .. ind .. "'", 2);
	end;
	if ind >= #self or ind < -#self then
		error("list index out of range", 2);
	end;
	if ind < 0 then
		proxy[self].data[(#self + ind) + 1] = val;
	end;
	proxy[self].data[ind + 1] = val;
end;

-- Constructor
function list.new(arr)
	if type(arr) ~= "table" and not proxy[arr] then
		error("bad argument #1 (table expected, got" .. typeof(list) .. ")", 2);
	end;
	local arr_copy = { };
	if type(list) == "table" then
		local i = 1;
		for ind, val in next, arr do
			-- Check
			if ind ~= i then
				error("bad argument #1 (array expected, got dictionary)", 2);
			end;
			
			arr_copy[ind] = val;
			i = i + 1;
		end;
	elseif proxy[arr].list then
		return arr;
	else
		for _, val in arr do
			table.insert(arr_copy, val);
		end;
	end;
	
	local pointer = newproxy(true);
	proxy[pointer] = { list = true, data = arr_copy };
	addr[pointer] = tostring(pointer):sub(11);
	
	local pointer_metatable = getmetatable(pointer);
	pointer_metatable.__metatable = "The metatable is locked";
	pointer_metatable.__index = getattr_list;
	pointer_metatable.__newindex = setattr_list;
	
	-- Operators
	pointer_metatable.__le = le;
	pointer_metatable.__lt = lt;
	pointer_metatable.__eq = eq;
	pointer_metatable.__add = concat_list;
	pointer_metatable.__concat = concat_list;
	pointer_metatable.__mul = mul_list;
	
	-- Iterator
	local ind = 0;
	pointer_metatable.__call = function()
		ind = ind + 1;
		if ind > #proxy[pointer].data then
			ind = 0;
			return;
		end;
		return ind - 1, proxy[pointer].data[ind];
	end;
	
	-- Other
	pointer_metatable.__len = len;
	pointer_metatable.__tostring = tostr_list;
	
	return pointer;
end;

function list.pack(...)
	return list.new { ... };
end;

-- Methods
setmetatable(list, { __newindex = function(self, ind, func) rawset(self, ind, 
	function(self1, ...) if (not proxy[self1]) then error(self == module and ("bad argument #1 (list expected, got " .. typeof(self) .. ')')
	or ("Expected ':' not '.' calling member function " .. ind), 2); end; return func(self1, ...) end); end; });

function list:append(value)
	table.insert(proxy[self].data, value);
	return self;
end;

function list:insert(ind, value)
	if type(ind) ~= "number" then
		error("bad argument #1 (integer expected, got" .. typeof(ind) .. ")", 2);
	elseif ind % 1 ~= 0 then
		error("bad argument #1 (integer expected, got float)", 2);
	end;
	if ind < 0 then
		table.insert(proxy[self].data, math.max((#self + ind) + 1, 1), value);
	else
		table.insert(proxy[self].data, math.min(ind + 1, #self), value);
	end;
end;

function list:pop(ind)
	if type(ind) ~= "number" then
		error("bad argument #1 (integer expected, got" .. typeof(ind) .. ")", 2);
	elseif ind % 1 ~= 0 then
		error("bad argument #1 (integer expected, got float)", 2);
	end;
	if ind < 0 then
		return table.remove(proxy[self].data, math.min((#self + ind) + 1, 1));
	end;
	return table.remove(proxy[self].data, math.max(ind + 1, #self));
end;

function list:unpack()
	return unpack(proxy[self].data);
end;

function list:filter(func, self_arg)
	if type(func) ~= "function" then
		error("bad argument #2 (function expected, got " .. typeof(func) .. ')', 2);
	end;
	local ret = { };
	
	for _, val in ipairs(proxy[self].data) do
		if self_arg and func(self, val) or func(val) then
			table.insert(ret, val);
		end;
	end;
	
	return list.new(ret);
end;

function list:index(val)
	return table.find(proxy[self].data, val);
end;

function list:count(val, self_arg)
	if self_arg ~= nil then
		if type(val) ~= "function" then
			error("bad argument #2 (function expected, got " .. typeof(val) .. ')', 2);
		end;
	end;
	local ret = 0;
	
	for _, val1 in ipairs(proxy[self].data) do
		if self_arg ~= nil then
			if self_arg and val(self, val1) or val(val1) then
				ret = ret + 1;	
			end;
		elseif val == val1 then
			ret = ret + 1;
		end;
	end;
	
	return ret;
end;

function list:sort(comp)
	return list.new(table.sort(proxy[self].data, comp));
end;

function list:copy()
	return list.new(table.move(proxy[self].data, 1, #self, 1, table.create(#self)));
end;

function list:reverse()
	if #self > 1 then
		for i = 0, math.floor(#self / 2) - 1 do
			self[i], self[-(i + 1)] = self[-(i + 1)], self[i];
		end;
	end;
	return self;
end;

function list:clear()
	for _ = 1, #self do
		table.remove(proxy[self].data);
	end;
	return self;
end;

function list:join(sep)
	return table.concat(proxy[self].data, sep or ',');
end;

function list:splice(i, c, ...)
	if type(i) ~= "number" then
		error("bad argument #1 (integer expected, got" .. typeof(i) .. ")", 2);
	elseif i % 1 ~= 0 then
		error("bad argument #1 (integer expected, got float)", 2);
	end;
	if type(c) ~= "number" then
		error("bad argument #2 (integer expected, got" .. typeof(c) .. ")", 2);
	elseif c % 1 ~= 0 then
		error("bad argument #2 (integer expected, got float)", 2);
	end;
	local args = { ... };
	if i < 0 then
		i = math.max((#self + i) + 1, 1);
	else
		i = math.min(i + 1, #self);
	end;
	for _ = 1, c do
		table.remove(proxy[self].data, c);
	end;
	for ind = #args, 1, -1 do
		table.insert(proxy[self].data, i, args[ind]);
	end;
	return self;
end;

function list:formatenglish()
	if #self == 1 then
		return self[0];
	end;
	return table.concat(proxy[self].data, ', ', 1, -2) .. ' and ' .. self[-1];
end;

return module;
1 Like

Thank you very much for this.
I have posted this as it’s easier to read on pastebin due to the side scrolling being omitted
(listed privately.)

You really shouldn’t do this - iterating over such a table with ipairs (which is what should be used to iterate over sequences) will result in the first element being skipped! In Lua, the first key of a table representing a sequence should always be equal to 1. Yes, it’s funky compared to most other languages, and sure, a numeric for can iterate this list properly, but I - and hopefully many others! - expect ipairs to work for all sequences.