Back

Activity 1.9: Tables as Lists

divider

The Idea

Lua has one data structure. Not one per job — one, total. It is called a table, and it does the work of arrays, dictionaries, objects, classes, modules, and namespaces. This activity uses it as a list. The next two use the same thing for entirely different purposes.

Positions start at 1. Not 0. This is the single most quoted fact about Lua and the one most likely to trip you.

main.lua
local crew = { "Ada", "Bo", "Cy" }
print(crew[1])
print(crew[3])
print(#crew)
print(crew[4])
Output
Ada
Cy
3
nil

#crew counts the items — the same # you used on strings in Activity 1.3. Because counting starts at 1, the last item is at crew[#crew], not crew[#crew - 1], which is one of the few places 1-indexing is genuinely tidier.

And reading past the end is not an error. You get nil, exactly like the misspelled variable in Activity 1.2. Same silence, same class of bug.

Walking a list

ipairs hands you the position and the value together, in order, from 1 until it hits a gap.

main.lua
local crew = { "Ada", "Bo", "Cy" }
for i, name in ipairs(crew) do
print(i, name)
end
Output
1 Ada
2 Bo
3 Cy

This is the fourth loop form from Activity 1.8, and ipairs is an ordinary function returning a closure — nothing about it is built into the language. If you do not need the position, the convention is to name it _, which is just a variable name that means "ignore me."

Adding and removing

main.lua
local crew = { "Ada", "Bo", "Cy" }
table.insert(crew, "Dee")
table.insert(crew, 1, "Zed")
print(table.concat(crew, ", "))
table.remove(crew)
table.remove(crew, 1)
print(table.concat(crew, ", "))
Output
Zed, Ada, Bo, Cy, Dee
Ada, Bo, Cy

table.insert(t, v) appends. Give it a position first and it inserts there, shuffling everything else along. table.remove(t) takes the last one off; with a position it removes that one and closes the gap. table.concat joins a list into a string, which is the tidiest way to look at one.

Sorting

table.sort rearranges the list in place — it returns nothing, so local s = table.sort(t) gives you nil and is a common first mistake.

main.lua
local scores = { 40, 12, 87, 3 }
table.sort(scores)
print(table.concat(scores, " "))
table.sort(scores, function(a, b) return a > b end)
print(table.concat(scores, " "))
Output
3 12 40 87
87 40 12 3

The second call passes a function that answers should a come before b? — and this is Activity 1.7's "functions are values" doing real work. Swap > for < and the order flips.

The trap that will bite you in Unit 2

Removing items while looping forwards is broken, and it fails quietly. Every time you remove one, everything after it slides down a position — but the loop counter keeps going up, so it steps straight over an item.

Forwards — wrong
local nums = { 2, 4, 5, 6 }
for i = 1, #nums do
local value = nums[i]
if value ~= nil and value % 2 == 0 then
table.remove(nums, i)
end
end
print(table.concat(nums, " "))
Output
4 5
Backwards — right
local nums = { 2, 4, 5, 6 }
for i = #nums, 1, -1 do
if nums[i] % 2 == 0 then
table.remove(nums, i)
end
end
print(table.concat(nums, " "))
Output
5

Both were asked to remove every even number. The forwards version left a 4 behind. It removed the 2, everything shifted down, and the 4 landed in the position the loop had already passed.

Loop backwards whenever you remove. Positions after the one you removed are the only ones that move, and going backwards you have already dealt with those. This is not a style preference — it is the difference between a bullet disappearing and a bullet surviving, and in Unit 2 you will write this exact loop.

Note the forwards version also needed a value ~= nil guard to avoid crashing outright, because the list got shorter than the count the loop started with. The backwards version needs no guard at all.

divider

Build

Start a fresh main.lua. You are keeping a crew roster, then filtering a list of scores.


Task 1: Build and read a roster

  • Make a list of three crew names.
  • Print the size, the first name, and the last name — get the last one without typing a 3.
  • Then list them all with ipairs.
main.lua
-- main.lua - Skynest crew roster
local crew = { "Ada", "Bo", "Cy" }
print("Crew size:", #crew)
print("First:", crew[1])
print("Last:", crew[#crew])
for i, name in ipairs(crew) do
print(i, name)
end
Output
Crew size: 3
First: Ada
Last: Cy
1 Ada
2 Bo
3 Cy

Task 2: Crew changes

  • Append a fourth name.
  • Insert a fifth at the front.
  • Print the whole roster on one line with table.concat.
  • Then remove whoever is at the front and print it again.
main.lua
-- main.lua - Skynest crew roster
local crew = { "Ada", "Bo", "Cy" }
print("Crew size:", #crew)
print("First:", crew[1])
print("Last:", crew[#crew])
for i, name in ipairs(crew) do
print(i, name)
end
table.insert(crew, "Dee")
table.insert(crew, 1, "Zed")
print("Roster:", table.concat(crew, ", "))
table.remove(crew, 1)
print("After launch:", table.concat(crew, ", "))
Output
Crew size: 3
First: Ada
Last: Cy
1 Ada
2 Bo
3 Cy
Roster: Zed, Ada, Bo, Cy, Dee
After launch: Ada, Bo, Cy, Dee

Task 3: Filter a list safely

  • Make a list of scores: { 12, 40, 7, 88, 5 }.
  • Remove every score below 10.
  • Loop backwards. Then, once it works, change it to loop forwards and see what you get. Keep the broken version in a comment — it is the most useful thing on this page.
main.lua
-- main.lua - Skynest crew roster
local crew = { "Ada", "Bo", "Cy" }
print("Crew size:", #crew)
print("First:", crew[1])
print("Last:", crew[#crew])
for i, name in ipairs(crew) do
print(i, name)
end
table.insert(crew, "Dee")
table.insert(crew, 1, "Zed")
print("Roster:", table.concat(crew, ", "))
table.remove(crew, 1)
print("After launch:", table.concat(crew, ", "))
local scores = { 12, 40, 7, 88, 5 }
for i = #scores, 1, -1 do
if scores[i] < 10 then
table.remove(scores, i)
end
end
print("Qualified:", table.concat(scores, " "))

Challenge (Optional)

  • Sort the surviving scores highest first, using a comparison function.
  • Write contains(list, value) returning true or false. Then make it return the position instead, or nil — which is more useful and costs nothing extra.
  • Set crew[2] = nil and then print #crew and loop it with ipairs. A hole in the middle of a list breaks both — which is exactly why table.remove exists instead.
divider

Check Your Work

Your finished file should produce this.

Output
Crew size: 3
First: Ada
Last: Cy
1 Ada
2 Bo
3 Cy
Roster: Zed, Ada, Bo, Cy, Dee
After launch: Ada, Bo, Cy, Dee
Qualified: 12 40 88

Qualified is 12 40 88 — both the 7 and the 5 are gone. If either survived, your loop was going forwards.