All About Tables (the first of two tutorials on the subject)

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.

41 Likes

Missing just a small detail, NaN also can’t be a key.

1 Like

Generally, I associate nil with NaN, but I realise that they’re not the same. Thanks for the clarification. I’ll edit that in immediately.

2 Likes

Worth noting: table.pack also sets n to the amount of arguments provided.

local packed = table.pack(nil,nil,nil)
print(#packed)  --> 0
print(packed.n) --> 3

ipairs also stops on the first hole in the array.

This wont work if the array x is too long (unpack can only unpack so many items), table.move should be used instead.

local function copyArray(array)
    local length = #array
    return table.move(array,1,length,1,table.create(length))
end

(might want to mention table.create and table.find too)

table.unpack and table.concat allow for range arguments i and j (default for i is 1, and default for j is #array)

local array = {1,2,3,4,5,6,7,8,9,10}
print(table.unpack(array,8))       --> 8 9 10
print(table.unpack(array,5,7))     --> 5 6 7
print(table.concat(array," ",7))   --> 7 8 9 10
print(table.concat(array,nil,5,7)) --> 567
1 Like

Thanks for the additional information - learning is a beautiful thing. For your second point, I’m pretty sure I cover that in the tutorial without specifically calling it a “hole.” As for your third point, since this is for beginners, I didn’t really feel like mentioning the unpack limit. I mean, it’s really rare that anyone has the desire to get a tuple of that length anyway. In addition to that, I did say I’d be making a second part, hence the missing functions from the table library. I’m also not entirely sure that those would apply here, since Roblox uses Lua5.1, but you know more than myself, no doubt (I have been away from Roblox for a while now). Any other specifics, including the n field added when using table.pack will be covered in the second part (I felt this part was getting long enough…). For others responding to my post, please continue with the feedback. It not only helps me improve on this post, but it also allows me to prepare for my next tutorial.

Roblox backported table.pack, table.unpack, and table.move. They also added table.create and table.find.

It is safe to use table.remove if you iterate over the table in reverse order:

for i = #t, 1, -1 do
    if someCondition(t[i]) then
        table.remove(t, i)
    end
end

Hi,
How to make a table inside a table? I am so confused about how to make it. For ex
print(TestTable[1][2])

There’s two things that are wrong there

  • That example you have shown only prints what’s on the table’s first and second input.
  • It is not posible making a table inside a table, Witch what you would be doing is create a variable inside a variable.

But there is one possible answer.

  • You can declare a variable table and then put the variable inside a table

However, It’s something like this:

local tableOne = {one, two}
local tableTwo = {three, four}
local tableThree = {tableOne[1], tableTwo[2]}

print (tablethree[1])
end

This is wrong. Tables can contain references to other tables aswell.

local x = { { }, { }, { }, { } }

For the first point, you should probably quote the specific example since OP has many examples

Remember that tables are a type made to contain other types. Since tables are types as well, you can just put them within other tables like you would any other type:

-- using the name you used
local TestTable = {
     {
          "Here,";
          "This is another example.";
     };
}
print(TestTable[1][2]) --> output --> This is another example.

You can put tables in tables in tables. It’s just a matter of what you’re trying to do. It’s not often necessary to stack more than 2 or 3. The need for that could be an indication of flawed logic. However, I’ll give another example so you can see more clearly how these things function:

local t = {
     {
          {
               "A"
          };
          {
               "Hi."            
          };
     };
     {
          "Oh."
     };
     "Bob."
}
print(t[1][1]) --> A
print(t[1][2]) --> Hi.
print(t[2][1]) --> Oh.
print(t[3]) --> Bob.

It’s also important to remember that you can use references to tables:

local t = {"What?"}
local t_two = {t}
print(t[1][1]) --> What?

I’m only using numeric indices in these examples because your question only included an example using numeric indices. It’s important to note that you can also use any of the other valid keys that I explain in my tutorial. If you’d like a different/more specific example, just let me know. I hope this helps! :smiley:

I think he was referring to the question with that first point, not my tutorial.

That’s incorrect.

local data = {}
local tools = {}
local ids = {}

for i,v in pairs(player_items:GetChildren()) do
    table.insert(tools, v.Name)
    table.insert(ids, v.ID)
    table.insert(data, tools)
    table.insert(data, ids)
end

-- Data now contains:
data = {
    {
        "Wooden Pickaxe",
        "Golden Gun"
    },
    {
        1,
        2
    }
}
1 Like