Back

Activity 1.10: Tables as Records

divider

The Idea

Same table, different job. Instead of numbering the slots, you name them — and now one table can describe a whole thing rather than a sequence of things.

main.lua
local ship = { name = "Skynest", crew = 4, online = true }
print(ship.name)
print(ship["crew"])
print(ship.captain)
Output
Skynest
4
nil

ship.name and ship["name"] are the same thing written two ways. The dot is shorthand and only works for names you could use as a variable; the brackets work for anything, which is what you need when the key is in a variable: ship[whichField].

A key that was never set is nil. By now you should expect that, and you should also expect that nobody warns you. A typo in a field name behaves exactly like a typo in a variable name.

There is no difference between the two uses

This is worth stating plainly, because it is where Lua is genuinely unlike most languages. A list and a record are the same kind of thing. { "Ada", "Bo" } is shorthand for a table with keys 1 and 2. The numbers were keys all along.

Which means one table can hold both at once, and # and ipairs simply ignore the named keys. That is occasionally useful and more often confusing, so keep your lists and your records separate until you have a specific reason not to.

Walking a record

pairs visits every key, named or numbered. Its sibling ipairs only walks 1, 2, 3 and stops.

main.lua
local ship = { name = "Skynest", crew = 4, online = true }
for key, value in pairs(ship) do
print(key, value)
end
Output — your order will differ
online true
crew 4
name Skynest

Read that caption again: the order is not guaranteed. It is not alphabetical, it is not the order you typed them, and it can differ between one Lua and another — the same file gives a different order in plain Lua than it does in LÖVE.

So never rely on it. If you need a predictable order, collect the keys into a list, sort that, and walk the sorted list. It sounds laborious and it is four lines. You will write it in Task 3, and it is the standard Lua idiom rather than a workaround.

Records inside records

A value can be another table, all the way down. This is how you describe anything with structure.

main.lua
local ship = {
name = "Skynest",
bay = { docked = 2, capacity = 6 },
}
print(ship.bay.docked)
print(ship.bay.capacity)
ship.bay.docked = 3
print(ship.bay.docked)
Output
2
6
3

One caution. ship.bay.docked only works if ship.bay actually exists. If it is nil, you are asking nil for a field, and that finally is a real error — one of the few things in Lua that stops your program rather than shrugging. Activity 1.12 covers what to do about it.

Keys come and go

Records are not fixed shapes. Assigning to a new key adds it, and assigning nil removes it — there is no separate delete.

main.lua
local ship = { name = "Skynest" }
ship.charge = 98
print(ship.charge)
ship.charge = nil
print(ship.charge)
Output
98
nil

"Set to nil" and "was never there" are indistinguishable, which is the price of not having a delete. It is also why pairs skips it afterwards — as far as the table is concerned, the key is gone.

The payoff: your globals were a table the whole time

Activity 1.2 warned you that leaving off local makes a variable global. Here is what that actually means.

main.lua
shipName = "Skynest"
print(_G.shipName)
print(_G["shipName"])
Output
Skynest
Skynest

Every global is a key in one ordinary table called _G. Writing shipName = 'Skynest' is a table assignment. So is calling print — it is a lookup of the key print in _G, and string.format is a lookup of format in a table stored under string.

Two things fall out of that. A misspelled global returns nil because you looked up a key that was not there — it was never a special case. And globals are slower than locals because every use costs a table lookup, while a local is just a slot.

Do not write to _G on purpose. It is here so the language stops looking like it has arbitrary rules. Nothing in Lua is a special case if you know it is tables the whole way down.

divider

Build

Start a fresh main.lua. You are describing one ship properly, rather than with five loose variables like Activity 1.2 did.


Task 1: Describe the ship

  • Make one table with a name, a crew count, and an online flag.
  • Print the name and the crew using dot access.
  • Print ship.captain, which you never set. Confirm it is not an error.
main.lua
-- main.lua - Skynest ship record
local ship = {
name = "Skynest",
crew = 4,
online = true,
}
print(ship.name)
print(ship.crew)
print(ship.captain)
Output
Skynest
4
nil

Task 2: Put a table inside it

  • Add a bay field holding its own table with docked and capacity.
  • Print both of those in one line.
  • Dock one more ship by adding 1 to the nested field, then print it again.
main.lua
-- main.lua - Skynest ship record
local ship = {
name = "Skynest",
crew = 4,
online = true,
bay = { docked = 2, capacity = 6 },
}
print(ship.name)
print(ship.crew)
print(ship.captain)
print("Docked:", ship.bay.docked, "of", ship.bay.capacity)
ship.bay.docked = ship.bay.docked + 1
print("Docked:", ship.bay.docked, "of", ship.bay.capacity)
Output
Skynest
4
nil
Docked: 2 of 6
Docked: 3 of 6

Task 3: Add a key, delete a key, list them in order

  • Add a charge field after the fact and print it.
  • Delete the online field by setting it to nil.
  • Collect the remaining keys into a list, sort it, and print each key next to its type().
  • Do this with the sorted-keys idiom, not with bare pairs — otherwise your output will not match, and it will not match differently on different machines. This task is Activities 1.9 and 1.10 combined, which is the point.
main.lua
-- main.lua - Skynest ship record
local ship = {
name = "Skynest",
crew = 4,
online = true,
bay = { docked = 2, capacity = 6 },
}
print(ship.name)
print(ship.crew)
print(ship.captain)
print("Docked:", ship.bay.docked, "of", ship.bay.capacity)
ship.bay.docked = ship.bay.docked + 1
print("Docked:", ship.bay.docked, "of", ship.bay.capacity)
ship.charge = 98
print("Charge:", ship.charge)
ship.online = nil
local keys = {}
for key in pairs(ship) do
table.insert(keys, key)
end
table.sort(keys)
for _, key in ipairs(keys) do
print(key, type(ship[key]))
end

Challenge (Optional)

  • Replace the bare pairs loop with one that prints only the fields that are not tables, so nested structures do not clutter the readout.
  • Make a list of ships, each one a record, and print a line per ship. This is the shape almost every game's data has, and it is just Activity 1.9 holding Activity 1.10.
  • Print type(_G) and _G.print == print. Both answers should now be unsurprising.
divider

Check Your Work

Your finished file should produce this.

Output
Skynest
4
nil
Docked: 2 of 6
Docked: 3 of 6
Charge: 98
bay table
charge number
crew number
name string

Four keys, alphabetical, and online is not among them. Note that bay reports as a table — a field holding a whole other structure looks no different from the outside, which is the property the next activity builds on.