Tables
The one data structure of Brass: lists, dictionaries, records, sets and objects, how to go through them, and what they cost.
A table is a container: it holds values, each one stored under a key. It is the only data structure of Brass, and it does every job. With keys 1, 2, 3... it is a list (the trains of a line, the steps of a sequence). With names as keys it is a dictionary or a record (the stock of each item, the settings of a machine). The libraries use tables everywhere too: chest.list() gives a table of slots, os.pull_event an event table, term.get_size a table {w = ..., h = ...}.
local line = {"press", "mixer", "saw"} -- a list
local press = {kind = "press", rpm = 64} -- a record
print(line[1], #line, press.rpm)press 3 64
Curly braces {} build a table. This page assumes you know values, loops and functions; the table reference details every function of the table library.
Lists
A list is a table whose keys are 1, 2, 3... in order. Write the values between braces, separated by commas; read one with its position in square brackets. Positions start at 1, not 0. Reading a position that holds nothing gives nil, without an error. #list is the length of the list.
The table library does the usual work on lists:
| call | does |
|---|---|
table.insert(list, value) | adds value at the end (same as list[#list + 1] = value) |
table.insert(list, pos, value) | inserts at position pos, shifting the next ones up |
table.remove(list) | removes the last element and returns it (nil on an empty list) |
table.remove(list, pos) | removes the element at pos, shifting the next ones down, and returns it |
table.concat(list, sep) | joins strings and numbers into one string |
table.sort(list) | sorts in place, smallest first (see functions as values for other orders) |
table.contains(list, value) | true if the value is in the table |
local queue = {"iron", "gold"}
table.insert(queue, "copper") -- at the end
table.insert(queue, 1, "coal") -- at the front
print(#queue, table.concat(queue, ", "))
local first = table.remove(queue, 1)
print(first, table.concat(queue, ", "))
print(queue[1], queue[10])4 coal, iron, gold, copper coal iron, gold, copper iron nil
A position outside the list, like 0 or 11 here, is not an error when reading; table.insert and table.remove refuse one that is out of the list with bad argument #2 to 'insert' (position out of bounds) (or 'remove'). So table.remove(list, 1) on an empty list is an error, while table.remove(list) just gives nil.
A list that keeps only its last entries is a small log: add at the end, and remove the oldest when it gets too long. A data logger keeps the last hour of a sensor this way:
local log = {}
local function record(value)
table.insert(log, value)
if #log > 5 then
table.remove(log, 1) -- forget the oldest
end
end
for reading = 10, 80, 10 do
record(reading)
end
print(table.concat(log, " "))40 50 60 70 80
Holes and the length
# is reliable only for a list without holes (a nil between two values). Brass keeps the list part of a table in one piece: # counts up to its last element, and only shrinks when that last element is removed. Setting an element in the middle to nil leaves a hole that # still counts, while ipairs stops at it:
local line = {"press", "mixer", "saw", "drill"}
line[2] = nil -- a hole
print(#line)
for i, machine in ipairs(line) do
print(i, machine)
end
local odd = {"a", nil, "c"}
print(#odd)4 1 press 1
In {"a", nil, "c"} the "c" does not even join the list part: #odd is 1. (Lua programmers know the result of # as a border, any one of which Lua may return when there are holes. In Brass it is always the size of the list part, so the results above are the same on every run.) The rule is simple: never leave holes in a list. To delete an element, use table.remove, which closes the gap. When a table has gaps by nature (the slots of a chest, where empty slots are missing), do not treat it as a list: go through it with pairs (see going through a table).
Removing while going through
Removing elements from a list while walking it forwards skips some: after a removal, the next element moves back into the position you just handled. Walk the list backwards instead, from #list down to 1:
local loot = {"cobblestone", "cobblestone", "diamond", "cobblestone"}
for i = #loot, 1, -1 do
if loot[i] == "cobblestone" then
table.remove(loot, i)
end
end
print(table.concat(loot, ", "))diamond
Dictionaries: any key
A key does not have to be a number. Any value except nil can be a key: a string most of the time, but also a number, a boolean, even another table. A table used this way is a dictionary: you look a value up by its key.
local stock = {}
stock["minecraft:iron_ingot"] = 128
stock["create:brass_ingot"] = 40
stock.coal = 12 -- same as stock["coal"]
local item = "create:brass_ingot"
print(stock[item], stock.coal, stock.diamond)
stock.coal = nil -- removes the key
for name, count in pairs(stock) do
print(name, count)
end40 12 nil minecraft:iron_ingot 128 create:brass_ingot 40
t.nameis a shortcut fort["name"], for keys that are valid names. Use brackets when the key is in a variable (stock[item]), contains other characters (stock["minecraft:iron_ingot"]), or is a number.- Beware:
t.itemis the key"item", whilet[item]uses the value of the variableitem. - Reading a missing key gives
nil. Giving a key the valuenilremoves it from the table. t[nil] = 1is an error:table index is nil.1and"1"are two different keys: a number read from text must go throughtonumberbefore it finds the number key.1and1.0are the same key.
In a table constructor, name = value sets a key that is a valid name, and [expression] = value sets any other key:
local recipes = {
["minecraft:iron_ingot"] = "smelt raw iron",
["create:brass_ingot"] = "mix copper and zinc",
[64] = "a full stack",
}
print(recipes["create:brass_ingot"])
print(recipes[64])mix copper and zinc a full stack
# counts only the list part: the length of a dictionary is 0. To count its entries, go through it with pairs.
Nested tables
A value in a table can be another table, as deep as you need. A whole factory fits in one table:
local factory = {
name = "Brass Works",
machines = {
{kind = "press", rpm = 64, running = true},
{kind = "mixer", rpm = 128, running = false},
},
storage = {iron = 512, zinc = 96},
}
print(factory.machines[2].kind, factory.storage.zinc)
factory.machines[2].running = true
print(#factory.machines, factory.machines[2].running)mixer 96 2 true
Reading through a missing level fails: factory.depot.size gives attempt to index a nil value (field 'depot'), because factory.depot is nil. Guard it with factory.depot and factory.depot.size, or check the level first.
References, not copies
A variable does not contain a table: it refers to it. Assigning a table to another variable, or passing it to a function, does not copy it: both names reach the same table, and a change through one is seen through the other. That is how a function can fill a table it receives.
local vault = {iron = 64}
local same = vault -- the same table, two names
same.iron = 0
print(vault.iron)
local copy = table.copy(vault)
copy.iron = 99
print(vault.iron, copy.iron)
print(vault == same, vault == copy)
print({} == {})0 0 99 true false false
==on tables compares identity: two tables are equal only if they are the same table, even when their contents match. To compare contents, compare the fields.table.copymakes a shallow copy: a new table with the same keys and values. A table inside is not copied; the copy refers to the same inner table. For a full copy, copy the inner tables too:
local function deep_copy(t)
local result = {}
for k, v in pairs(t) do
if type(v) == "table" then
v = deep_copy(v)
end
result[k] = v
end
return result
end
local plan = {name = "line A", speeds = {64, 128}}
local backup = deep_copy(plan)
plan.speeds[1] = 0
print(backup.speeds[1])64
(A table that contains itself would make deep_copy recurse forever, until stack overflow.)
Going through a table
Three loops, already met in Conditions and loops:
| loop | visits | use it for |
|---|---|---|
for i, v in ipairs(t) do | t[1], t[2]... until the first nil | lists |
for v in t do | the same values, without the position | lists, when the position does not matter |
for k, v in pairs(t) do | every key | everything else |
The order of pairs is predictable in Brass (in Lua it is not): first the list part, positions 1, 2, 3... in order, then all the other keys in the order they were first added. A key removed and added again goes to the end. Changing or removing existing keys during a pairs loop is safe; keys added during the loop may not be visited.
chest.list() (inventory.list()) is the typical table with gaps: it has a key for each slot that holds something, and none for empty slots. ipairs would stop at the first empty slot; pairs sees them all:
-- what chest.list() returns: only the slots that hold something
local slots = {}
slots[1] = {name = "minecraft:iron_ingot", count = 64}
slots[2] = {name = "minecraft:iron_ingot", count = 12}
slots[5] = {name = "create:andesite_alloy", count = 30}
print("#slots = " .. #slots)
for slot, item in pairs(slots) do
print(slot, item.count, item.name)
end#slots = 2 1 64 minecraft:iron_ingot 2 12 minecraft:iron_ingot 5 30 create:andesite_alloy
Each item also has a display field, its name as shown in game. table.keys(t) returns a list of the keys of a table, handy to sort them before showing them.
Records: a list of machines
A record is a table with named fields that describes one thing. A list of records describes many things of the same kind, and loops over it read like sentences:
local machines = {
{name = "Crusher A", rpm = 128, overstressed = false},
{name = "Crusher B", rpm = 0, overstressed = true},
{name = "Press", rpm = 64, overstressed = false},
}
for _, m in ipairs(machines) do
local state = "running"
if m.overstressed then
state = "OVERSTRESSED"
elseif m.rpm == 0 then
state = "stopped"
end
print(string.format("%-10s %4d RPM %s", m.name, m.rpm, state))
endCrusher A 128 RPM running Crusher B 0 RPM OVERSTRESSED Press 64 RPM running
With real machines, each record would be filled from the blocks themselves (@kinetic.speed, kinetic.overstressed()) and the list shown on a monitor: see the factory dashboard.
Objects: records with methods
Put functions in a record and it becomes an object: data plus the methods that work on it, called with :. To make many objects of the same kind, write the methods once in a table, and a function that builds each object and copies the methods into it. (Brass has no metatables, so each object carries its own references to the methods; the functions themselves are shared, not duplicated.)
local Tank = {}
function Tank:fill(mb)
self.amount = math.min(self.capacity, self.amount + mb)
end
function Tank:percent()
return math.floor(self.amount * 100 / self.capacity)
end
local function new_tank(fluid, capacity)
local tank = {fluid = fluid, amount = 0, capacity = capacity}
for name, method in pairs(Tank) do
tank[name] = method
end
return tank
end
local lava = new_tank("lava", 8000)
local water = new_tank("water", 4000)
lava:fill(6000)
lava:fill(1000)
water:fill(5000)
print(lava.fluid .. ": " .. lava:percent() .. " %")
print(water.fluid .. ": " .. water:percent() .. " %")lava: 87 % water: 100 %
Sets
A set answers one question: is this value in it? Use the values as keys and true as the value. Checking is a single lookup, allowed[name], however big the set is.
local allowed = {Steve = true, Alex = true}
allowed["Notch"] = true -- add
allowed.Alex = nil -- remove
for _, player in ipairs({"Steve", "Alex", "Herobrine", "Notch"}) do
if allowed[player] then
print(player .. ": welcome")
else
print(player .. ": access denied")
end
endSteve: welcome Alex: access denied Herobrine: access denied Notch: welcome
table.contains(list, value) gives the same answer on a list, but it looks through every element: fine for ten names, slower for a thousand. A set is the tool for a door that lets some players in.
Counting and grouping
To count how many times each value appears, use the value as a key and add one each time. The (counts[k] or 0) gives 0 the first time a key is seen:
local passed = {"Red Line", "Blue Line", "Red Line", "Freight", "Red Line", "Blue Line"}
local counts = {}
for _, train in ipairs(passed) do
counts[train] = (counts[train] or 0) + 1
end
for train, n in pairs(counts) do
print(train, n)
endRed Line 3 Blue Line 2 Freight 1
Grouping goes one step further: each key holds a table that collects everything belonging to it. Here, the contents of a chest grouped by mod, using the part of the item id before the ::
-- what chest.list() could return
local items = {
{name = "minecraft:iron_ingot", count = 64},
{name = "create:andesite_alloy", count = 30},
{name = "minecraft:coal", count = 12},
{name = "create:brass_ingot", count = 9},
{name = "computingages:copper_wire", count = 4},
}
local by_mod = {}
for _, item in pairs(items) do
local parts = item.name:split(":") -- {"minecraft", "iron_ingot"}
local mod = parts[1]
if by_mod[mod] == nil then
by_mod[mod] = {total = 0, kinds = {}}
end
local group = by_mod[mod]
group.total = group.total + item.count
table.insert(group.kinds, parts[2])
end
for mod, group in pairs(by_mod) do
print(mod .. ": " .. group.total .. " (" .. table.concat(group.kinds, ", ") .. ")")
endminecraft: 76 (iron_ingot, coal) create: 39 (andesite_alloy, brass_ingot) computingages: 4 (copper_wire)
The loop uses pairs rather than ipairs because the real chest.list() skips empty slots. string.split cuts the id in two (see Strings).
What tables cost
Each computer has a memory measured in cells, from 2,048 for a Tube Computer to 1,048,576 for a Modern Computer (131,072 for a Personal Computer). Tables are where most of it goes:
| thing | cells |
|---|---|
| a table | 4 |
| each entry of a table (key and value) | 2 |
| a string | 1, plus 1 per 8 characters |
A list of 1,000 sensor readings costs about 2,004 cells: nothing for a Personal Computer, the whole memory of a Tube Computer. Memory is given back on its own when nothing refers to a table any more (the local of a function that has returned, a field or a variable set to nil). A program that keeps more than the computer can hold stops with out of memory.
So keep only what you need: a rolling log rather than an endless one, and counts rather than the full list of events. os.memory tells how much is in use; Performance explains the budget of instructions and cells in detail.