I have decided to make a new tutorial that will (hopefully) be comprehensive in nature and address some common misconceptions on the topic of tables.
Quick disclaimer: None of this will involve direct visible application in Roblox. All of it will be usable in Roblox, but I’ve drifted away from developing in Roblox studio, and I’m not really interested in addressing the hundreds of possible applications in a game dev environment. Rather, this is meant to a place where you can grab some tools for your toolbox.
Let’s get started! First, I’d like to begin with arrays. An array is a structure in many languages that usually has an index and a value, where the indices start at zero and end wherever the last value in the array is. Note that because the starting index would be zero, if the last element in the array was (to us) at place five, it would really be at the fourth index. Now, in Lua, this is slightly different. In Lua, arrays have a starting index of one, which is more intuitive, but less useful than starting at zero could be, but I won’t get into that. Here’s an example of an array in Lua:
local array = {"a", "b", "c"} -- remember, you can name things whatever you like
In Lua, “a” would be at index one, “b” would be at index two, and “c” would be at index three. You might be wondering, “how is this useful and why do we need indices?” Well, it’s basically like a library. A library can have multiple books and they have a numbering or lettering system that allows you to find any particular book fairly easily. In the same way, an array can contain multiple values and have each value easily accessed by its index. Here’s another example with the same array, but this time indexing some of the values:
local array = {"a", "b", "c"}
print(array[1]) -- output --> a
print(array[3]) -- output --> c
As you can see, an array can be indexed by using the square brackets surrounding the integer which represents the index you’re looking for. If you’re curious as to whether or not you can index an array at a negative integer like array[-1]
, then don’t worry, we’ll cover that later. At that point, it’s not really an array, because the definition of an array (in Lua) is that indices are natural numbers that progress from our starting index (1) up to our ending index (whatever that may be). This means that true arrays cannot have negative indices. Dictionaries, however, can. But, as I said, we’ll cover that later.
At this point, you might be wondering why I’m using the term array
and not table
(if you’ve learned some of this in the past). The reason for this is that tables in Lua are a really flexible combination of arrays and dictionaries. Generally, you can think of a table as having an array
part and a dictionary
part. A table is more technically an “associative array.” You can see more on what that means here.
Now that we’re done with that interesting little tangent, I’ll continue with an explanation of dictionaries. In most languages, a dictionary is a collection of key-value
pairs. Each key is generally a string, and the value is whatever you choose to store. Some languages are really restrictive and force you to define what type
of values you are using for keys and values. You might to have string keys and integer values. Lua is more flexible, and you’re able to use anything but nil
and NaN
as a key. This opens a large number of possibilities, but I don’t usually see people using booleans as indices. Often the most useful indices (aka keys) are numbers or strings, hence arrays and dictionaries (this isn’t to say that dictionaries are or should be restricted to strings or numbers). Here are a few examples involving dictionaries:
local x = {
["a"] = 1;
["b"] = 2;
["c"] = 3; -- you could use commas instead of semicolons
}
print(x["a"]) -- output --> 1
print(x["b"]) -- output --> 2
-- you don't have to format the dictionary like I did, but it's harder to comprehend what is going on the more values you add:
local x = {["a"] = 1; ["b"] = 2; ["c"] = 3; ["d"] = 4; ["e"] = 5;}
Like I said in the comment above, you can use commas instead of semicolons. As a general rule, I use commas for arrays and semicolons for dictionaries. Both are working delimiters. It’s primarily aesthetical preference, so do what you like. You can also write the keys without the [“”], but it’s usually less aesthetically pleasing and doesn’t account for indices with spaces in them, but those are (or should be) rare. It’s usually easier to use _s for spaces in pretty much any language or file system where you navigate by using the terminal (kind of off topic, but whatever). Anyway, I’ll give an example of that and then how to use other types as indices (aka keys):
local x = {
a = 1;
b = 2;
c = 1;
}
print(x["a"], x["b"], x["c"]) -- note that you still have to index it as a string
-- output --> 1 2 1
local a_table_key = {1, 2, 3}
local y = {
what_am_i_doing_here = "shrug";
[21.2] = "happy";
[-32] = "joe"; -- I said we would get around to this
[true] = 1234567890;
[a_table_key] = "Oh my, we just indexed with a table.";
} -- as you can see, these "associative arrays" are extremely flexible
print(y[21.2]) -- output --> happy
print(y[a_table_key]) -- output --> Oh my, we just indexed with a table.
-- you can test the rest if you like
You can even store a table in a table, and this can actually prove really useful in some circumstances:
local x = { -- dictionary example
["y"] = {
["z"] = {
-- you could imagine this going on for a while
}
}
}
local a = { -- array example
{
"a";
"b";
"c";
}
}
At this point I’d like to make a quick but important note about indexing with tables. When indexing with tables, it’s not checking the content of the table to see if it matches that of the table you originally set the index to be. Therefore,
local x = {}
x[{"a"}] = true
print(x[{"a"}]) --> nil
The only way indexing with tables works (outside of using metamethods) is if you use the same table that you originally used as the index. The contents don’t need to be the same. This is because of the way tables are stored in memory. This behaviour can be simply explained with this example:
print({} == {}) --> false
That’s why I defined the table as a variable in the initial example:
local y = {}
local a_table_key = {}
y[a_table_key] = true
print(y[a_table_key]) --> true
The next structure is that of a mixed table. A mixed table (in layman’s terms) is a table that has both an array part and a dictionary part at the same time. It might look like this:
local x = {
"a";
["hah"] = 49;
3;
["joe"] = 12;
["steve"] = {
"a";
};
15;
}
a, 3, and 15 are “in the array part” and 49, 12, {“a”} are “in the dictionary part.”
I’m now going to quickly cover how you can insert a value into a table after it’s been created and then I’ll go over some ways to read values from tables. I’ll just give some examples because adding values to tables is pretty intuitive:
local x = {}
x[2] = "happy"
--[[
until you add a value to x at the index of one, x is considered a dictionary
because the definition of an array is that it is a collection of values that are
indexed consecutively from one to n, where n is the length of the table.
--]]
x[1] = "something else"
-- x is now an array
-- everything else has pretty much the same way of assigning a value to an index (aka key):
x[true] = 51
x["what"] = "I don't know"
-- etc.
-- you can always replace the value at a particular index in the same way:
x["what"] = "Now I know"
Lua also has a nice syntactical trick that allows you to index a table without the [“”]. You can simply use a .
and the key you’re trying to access. Lua basically interprets tbl.awordorsomething
as tbl["awordorsomething"]
.
local x = {
["happy"] = 1;
}
print(x.happy) -- output --> 1
-- for syntactical reasons, you cannot do things like x.true or x.2, even if
x["true"] = 2
-- and
x["2"] = 3
-- exist. It will just error.
Let us now quickly cover table.insert and table.remove. As the names suggest, one is for removing things from tables and the other is for adding (or inserting). I’m going to assume you already know about functions/arguments/parameters. If you don’t and would like me to make a tutorial all about functions, feel free to let me know. The first argument for table.insert
is the table you want to insert your value into, the second argument is the position (aka index), and the third/last argument is what you wish to insert. If you just provide two arguments, it’ll assume the second argument is the thing you want to insert and it’ll just increase the length of the table by one and add the thing you wish to add to the “top.” Here’s an example using both methods:
local x = {43, 72, 101, 13}
table.insert(x, 2, 51)
-- now the table x would be {43, 51, 72, 101, 13}
-- note that this isn't really efficient because it has to push all the following elements up by one
local y = {4, 3, 2}
table.insert(y, 1) -- now y will be {4, 3, 2, 1} because 1 was tossed on the top
table.remove
is similar, but with a different functionality. As with table.insert
, the first argument is the table, and the second argument is the position you want to remove at. If no argument is provided for position, it will just remove from the “top.” table.remove
returns the value it removed from the table, so you can store it in a variable or use it in an equation or something, if you want (this is useful in stacks, which I’ll cover in part 2). Here are a few examples using everything I just covered:
local x = {"a", "b", "c"}
table.remove(x, 2) -- x will now be {1, 3}
local y = {"d", "e", "f"}
table.remove(y) -- y will now be {"d", "e"}
local z = {"g", "h", "i"}
local something = table.remove(z)
-- something is now "i" and z is now {"g", "h"}
local something_else = table.remove(z, 1)
-- something_else is now "g" and z is now {"h"}
-- as with table.insert, if you specify where you want to remove for, expect some
-- inefficiency because of the need to shift things over to the left.
The specified indices must always be array compatible, meaning you can’t do table.insert(x, 2.3, "hah")
or table.insert(x, "something", 2)
. The same goes for table.remove
.
Now that I’ve covered some of the basics, lets discuss ways to read values from tables (I’m going to assume that you already know about numeric for loops). First I’ll cover table.concat
followed by iterating through an array using numeric for loops and generic for loops.
table.concat
is a simple method that returns a string which is all of the elements of a table mashed (not really technical language here, folks) together, or separated by an optional delimiter, which can be provided as the second argument. The first argument would be, as with all the methods we’ve covered so far, the table. If an element in the array is not able to be concatenated, an error will be thrown (it should tell you which value was invalid, though). One use case can be seen in ScriptGuider’s answer here. table.concat
does work on mixed tables, but it only “mashes” the array part of the table together. Here are a few simple examples that should give a clear idea of what table.concat
does:
local x = {1, 2, 3, 4, 5, 6, 7, "hi", "bye", "go", "no", "whatever"}
print(table.concat(x)) -- output --> 1234567hibyegonowhatever
-- there was no delimiter, so it looked kind of ugly. Let us add one:
print(table.concat(x, "\t"))
-- "\t" is a tab, "\n" would be a newline, ", " would be a comma
-- and space, etc. whatever is in the string will be your delimiter
-- output --> 1 2 3 4 5 6 7 hi bye go no whatever
table.concat(x, " ::: ") -- output --> 1 ::: 2 ::: 3 ::: 4 ::: 5 ::: 6 ::: 7 ::: hi ::: bye ::: go ::: no ::: whatever
-- you can experiment further if you wish
Before we move on to looping, I’d like to mention that if you index a table at a non-existent index, then you’ll just get nil. For example,
local x = {}
print(x[1]) -- output --> nil
Ok, I’ll start by explaining how to loop through the array part of a table via a numeric for loop or a generic for loop using the iterator function ipairs
. Remembering how to index a table, we can use a numeric for loop like so:
local x = {"a", "b", "c"}
-- remember, # is the way to get the length of a table (or a string)
for i = 1, #x do -- basically, I am looping until we reach the length of the table, aka the last index
print(i, x[i])
end
-- output
-- 1 a
-- 2 b
-- 3 c
As you can see, if we loop up to the length of the table, we can just index it at each value of i. You can extend this idea to dictionaries if the strings that are keys have some kind of consecutive sequence of names like joe1, joe2, joe3, etc. You could just do x["joe"..i]
, for example. To use the iterator function ipairs
, we have to use a slightly different syntactical structure in our for loop:
local x = {"a", "b", "c"}
for index, value in ipairs(x) do
print(index, value)
end
-- output --
-- 1 a
-- 2 b
-- 3 c
As expected, we have the same output, but it should be noted that the numeric for loop is much more efficient (especially with longer tables). Both of these methods guarantee order. What I mean by that is the indices in the output started at 1, then went to 2, then 3. That’s why I introduced these two methods first. There are two other iterator functions (next
and pairs
), but they don’t “guarantee” order. In an array, they usually do go in order, and both are more efficient than ipairs
, but they are both arbitrary iterator functions, so it’s always possible that they won’t go in order. ipairs
only loops over the array part of a table.
Edit for clarification (thanks to HaxHelper for the info): With Luau, the speeds are different than you’d expect when working with unmodified Lua. ipairs
(on Roblox) is now more efficient than the equivalent numeric for loop. This is a big positive, since ipairs
used to be relatively useless apart from readability reasons. Now it allows for readability and speed.
That’s why this happens (not talking about the edit above, but the paragraph above that):
local x = {}
x[1] = "Hey!"
x[2] = "Yo!"
x[4] = "Hello!"
for index, value in ipairs(x) do
print(index, value)
end
-- output --
-- 1 Hey!
-- 2 Yo!
-- remember that x[4] doesn't count as part of the array part of the table
-- because x[3] doesn't exist. If we add it, x[4] will output:
x[3] = "Yay!"
for index, value in ipairs(x) do
print(index, value)
end
-- output --
-- 1 Hey!
-- 2 Yo!
-- 3 Yay!
-- 4 Hello!
pairs
and next
will traverse all key-value pairs in a given table, thus they are usually used for iterating through dictionaries. If you didn’t know already, pairs
is really next
in disguise. The pairs
iterator is pretty much defined as
function pairs(t)
return next, t, nil
end
Either way, I’ll demonstrate how to use both in a generic for loop:
-- getting tired of using x
local some_cool_new_name = {
["a"] = 10;
["b"] = 20;
["c"] = 30;
"Hello."; -- this is a mixed table, as we covered before
}
for index, value in pairs(some_cool_new_name) do
print(index, value)
end
-- remember that pairs is arbitrary, and letters are not stored in "order" so it
-- may not output them in order.
-- output --
-- 1 Hello.
-- b 20
-- a 10
-- c 30
for index, value in next, some_cool_new_name do
print(index, value)
end
-- again, the output probably won't be the same as I'm writing it
-- output --
-- 1 Hello.
-- b 20
-- a 10
-- c 30
Some people will argue that using next
instead of pairs
is more efficient because it’s one less function call, but most experienced programmers will agree that that’s absurd, and it’s easier to see what’s going on with pairs, so that’s the suggested iterator function to use out of the two. However, if you’re already using one or the other, stick with it. Consistency in programming is key. If you’re wondering what would happen if I used the ipairs
iterator in the most recent situation, it’d just result in an output of 1 Hello
because that’s the only element in the array part of the table. You can also create your own iterator functions. If you look it up online, you’ll probably find a few good information sources/examples.
Now that I’ve covered iterator functions, I’ll quickly go over two other useful table related methods: table.unpack
and table.pack
(I think table.unpack is unpack in Roblox/Lua5.1). Like table.concat
, table.unpack
works on mixed tables and arrays, but it only “unpacks” the array part. Basically, the first argument for table.unpack
is a table, and table.unpack
returns all the elements of that table as a list separated by commas (the word tuple is confusing, so I just say a list of values separated by commas):
local x = {1, 2, 3}
print(table.unpack(x)) -- output --> 1 2 3
-- this can be a useful way to copy the array part of a table:
local x_copy = {table.unpack(x)}
table.pack
is really the opposite. It takes a list of comma separated values (aka a tuple) and returns a table which contains all of them:
-- we could really do the copy as demonstrated above but with these two methods together:
local x = {1, 2, 3}
local x_copy = table.pack(table.unpack(x))
-- and
local some_table = table.pack("Hello", "GoodBye", "Whatever")
A few final notes:
I don’t recommend using table.remove
while looping through a table. The iterator won’t account for the change in the length in the table and you’ll get unexpected results. For example,
local x = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
for index, value in ipairs(x) do
print(index, value)
table.remove(x, index)
end
-- output --
-- 1 10
-- 2 8
-- 3 6
-- 4 4
-- 5 2
Another thing to mention is that # only checks the length of the array part of a table, so if you want the full length of a table, you’re going to have to do something like this (this is an oversimplification of how # works. If you want more information, here you go):
local function get_table_length(tbl)
local counter = 0
for _ in pairs(tbl) do -- it's Lua convention to use _ as a placeholder if you don't need the value
counter = counter + 1
end
return counter
end
local x = {
["a"] = 1;
["b"] = 2;
["c"] = 3;
}
print(get_table_length(x)) -- output --> 3
Feel free to leave your criticisms and comments below. If I missed anything you deem important, be sure to tell me so that I can fix it. I hope this tutorial proves useful/helpful. Have a great day scripting!
Note: I do intend to write a part two someday. That is why I have left out some things like metatables
, table.sort
, and the concept of methods
in an oop style framework.