Tables
L'unique structure de données de Brass : listes, dictionnaires, fiches, ensembles et objets, comment les parcourir, et ce qu'elles coûtent.
Une table est un contenant : elle garde des valeurs, chacune rangée sous une clé. C'est l'unique structure de données de Brass, et elle fait tous les métiers. Avec les clés 1, 2, 3... c'est une liste (les trains d'une ligne, les étapes d'une séquence). Avec des noms comme clés, c'est un dictionnaire ou une fiche (le stock de chaque objet, les réglages d'une machine). Les bibliothèques aussi utilisent des tables partout : coffre.list() donne une table d'emplacements, os.pull_event une table d'évènement, term.get_size une table {w = ..., h = ...}.
local chaine = {"presse", "mixeur", "scie"} -- une liste
local presse = {genre = "presse", rpm = 64} -- une fiche
print(chaine[1], #chaine, presse.rpm)presse 3 64
Les accolades {} construisent une table. Cette page suppose que vous connaissez les valeurs, les boucles et les fonctions ; la référence table détaille chaque fonction de la bibliothèque table.
Les listes
Une liste est une table dont les clés sont 1, 2, 3... dans l'ordre. Écrivez les valeurs entre accolades, séparées par des virgules ; lisez-en une avec sa position entre crochets. Les positions commencent à 1, pas à 0. Lire une position vide donne nil, sans erreur. #liste est la longueur de la liste.
La bibliothèque table fait le travail habituel sur les listes :
| appel | effet |
|---|---|
table.insert(liste, valeur) | ajoute valeur à la fin (comme liste[#liste + 1] = valeur) |
table.insert(liste, pos, valeur) | insère à la position pos, en décalant les suivantes vers le haut |
table.remove(liste) | retire le dernier élément et le renvoie (nil sur une liste vide) |
table.remove(liste, pos) | retire l'élément en pos, en décalant les suivants vers le bas, et le renvoie |
table.concat(liste, sep) | colle des chaînes et des nombres en une seule chaîne |
table.sort(liste) | trie sur place, du plus petit au plus grand (voir les fonctions comme valeurs pour d'autres ordres) |
table.contains(liste, valeur) | true si la valeur est dans la table |
local file = {"fer", "or"}
table.insert(file, "cuivre") -- à la fin
table.insert(file, 1, "charbon") -- au début
print(#file, table.concat(file, ", "))
local premier = table.remove(file, 1)
print(premier, table.concat(file, ", "))
print(file[1], file[10])4 charbon, fer, or, cuivre charbon fer, or, cuivre fer nil
Une position hors de la liste, comme 0 ou 11 ici, n'est pas une erreur en lecture ; table.insert et table.remove refusent une position hors de la liste avec bad argument #2 to 'insert' (position out of bounds) (ou 'remove'). Ainsi, table.remove(liste, 1) sur une liste vide est une erreur, alors que table.remove(liste) donne simplement nil.
Une liste qui ne garde que ses dernières entrées fait un petit journal : on ajoute à la fin, et on retire la plus ancienne quand elle devient trop longue. Un enregistreur garde ainsi la dernière heure d'un capteur :
local journal = {}
local function noter(valeur)
table.insert(journal, valeur)
if #journal > 5 then
table.remove(journal, 1) -- oublie la plus ancienne
end
end
for mesure = 10, 80, 10 do
noter(mesure)
end
print(table.concat(journal, " "))40 50 60 70 80
Les trous et la longueur
# n'est fiable que pour une liste sans trou (un nil entre deux valeurs). Brass garde la partie liste d'une table d'un seul tenant : # compte jusqu'à son dernier élément, et ne diminue que quand ce dernier élément est retiré. Mettre un élément du milieu à nil laisse un trou que # compte encore, alors qu'ipairs s'y arrête :
local chaine = {"presse", "mixeur", "scie", "perceuse"}
chaine[2] = nil -- un trou
print(#chaine)
for i, machine in ipairs(chaine) do
print(i, machine)
end
local bancale = {"a", nil, "c"}
print(#bancale)4 1 presse 1
Dans {"a", nil, "c"}, le "c" ne rejoint même pas la partie liste : #bancale vaut 1. (Les habitués de Lua connaissent le résultat de # sous le nom de bordure, que Lua peut choisir parmi plusieurs quand il y a des trous. En Brass, c'est toujours la taille de la partie liste : les résultats ci-dessus sont les mêmes à chaque exécution.) La règle est simple : ne laissez jamais de trou dans une liste. Pour supprimer un élément, utilisez table.remove, qui referme l'espace. Quand une table a des trous par nature (les emplacements d'un coffre, où les emplacements vides manquent), ne la traitez pas comme une liste : parcourez-la avec pairs (voir parcourir une table).
Retirer en parcourant
Retirer des éléments d'une liste en la parcourant vers l'avant en fait sauter : après un retrait, l'élément suivant recule à la position qu'on vient de traiter. Parcourez plutôt la liste à l'envers, de #liste jusqu'à 1 :
local butin = {"cobblestone", "cobblestone", "diamant", "cobblestone"}
for i = #butin, 1, -1 do
if butin[i] == "cobblestone" then
table.remove(butin, i)
end
end
print(table.concat(butin, ", "))diamant
Les dictionnaires : n'importe quelle clé
Une clé n'est pas forcément un nombre. N'importe quelle valeur sauf nil peut servir de clé : une chaîne la plupart du temps, mais aussi un nombre, un booléen, et même une autre table. Une table utilisée ainsi est un dictionnaire : on y cherche une valeur par sa clé.
local stock = {}
stock["minecraft:iron_ingot"] = 128
stock["create:brass_ingot"] = 40
stock.charbon = 12 -- pareil que stock["charbon"]
local objet = "create:brass_ingot"
print(stock[objet], stock.charbon, stock.diamant)
stock.charbon = nil -- retire la clé
for nom, quantite in pairs(stock) do
print(nom, quantite)
end40 12 nil minecraft:iron_ingot 128 create:brass_ingot 40
t.nomest un raccourci det["nom"], pour les clés qui sont des noms valides. Utilisez les crochets quand la clé est dans une variable (stock[objet]), contient d'autres caractères (stock["minecraft:iron_ingot"]), ou est un nombre.- Attention :
t.objetest la clé"objet", alors quet[objet]utilise la valeur de la variableobjet. - Lire une clé absente donne
nil. Donner la valeurnilà une clé la retire de la table. t[nil] = 1est une erreur :table index is nil.1et"1"sont deux clés différentes : un nombre lu dans un texte doit passer partonumberavant de trouver la clé numérique.1et1.0sont la même clé.
Dans un constructeur de table, nom = valeur fixe une clé qui est un nom valide, et [expression] = valeur n'importe quelle autre clé :
local recettes = {
["minecraft:iron_ingot"] = "fondre du fer brut",
["create:brass_ingot"] = "mélanger cuivre et zinc",
[64] = "une pile complète",
}
print(recettes["create:brass_ingot"])
print(recettes[64])mélanger cuivre et zinc une pile complète
# ne compte que la partie liste : la longueur d'un dictionnaire vaut 0. Pour compter ses entrées, parcourez-le avec pairs.
Des tables dans des tables
Une valeur d'une table peut être une autre table, aussi profond qu'il le faut. Toute une usine tient dans une table :
local usine = {
nom = "Laitonnerie",
machines = {
{genre = "presse", rpm = 64, en_marche = true},
{genre = "mixeur", rpm = 128, en_marche = false},
},
stock = {fer = 512, zinc = 96},
}
print(usine.machines[2].genre, usine.stock.zinc)
usine.machines[2].en_marche = true
print(#usine.machines, usine.machines[2].en_marche)mixeur 96 2 true
Lire à travers un niveau absent échoue : usine.depot.taille donne attempt to index a nil value (field 'depot'), car usine.depot vaut nil. Protégez-vous avec usine.depot and usine.depot.taille, ou vérifiez d'abord le niveau.
Des références, pas des copies
Une variable ne contient pas une table : elle y fait référence. Affecter une table à une autre variable, ou la passer à une fonction, ne la copie pas : les deux noms mènent à la même table, et une modification faite par l'un se voit par l'autre. C'est ainsi qu'une fonction peut remplir une table qu'elle reçoit.
local coffre = {fer = 64}
local meme = coffre -- la même table, deux noms
meme.fer = 0
print(coffre.fer)
local copie = table.copy(coffre)
copie.fer = 99
print(coffre.fer, copie.fer)
print(coffre == meme, coffre == copie)
print({} == {})0 0 99 true false false
==sur des tables compare l'identité : deux tables ne sont égales que si c'est la même table, même quand leurs contenus se ressemblent. Pour comparer des contenus, comparez les champs.table.copyfait une copie superficielle : une nouvelle table avec les mêmes clés et valeurs. Une table à l'intérieur n'est pas copiée ; la copie fait référence à la même table intérieure. Pour une copie complète, copiez aussi les tables intérieures :
local function copie_profonde(t)
local resultat = {}
for k, v in pairs(t) do
if type(v) == "table" then
v = copie_profonde(v)
end
resultat[k] = v
end
return resultat
end
local plan = {nom = "chaîne A", vitesses = {64, 128}}
local sauvegarde = copie_profonde(plan)
plan.vitesses[1] = 0
print(sauvegarde.vitesses[1])64
(Une table qui se contient elle-même ferait tourner copie_profonde sans fin, jusqu'au stack overflow.)
Parcourir une table
Trois boucles, déjà vues dans Conditions et boucles :
| boucle | parcourt | à utiliser pour |
|---|---|---|
for i, v in ipairs(t) do | t[1], t[2]... jusqu'au premier nil | les listes |
for v in t do | les mêmes valeurs, sans la position | les listes, quand la position ne compte pas |
for k, v in pairs(t) do | toutes les clés | tout le reste |
L'ordre de pairs est prévisible en Brass (en Lua, il ne l'est pas) : d'abord la partie liste, positions 1, 2, 3... dans l'ordre, puis toutes les autres clés dans l'ordre où elles ont été ajoutées la première fois. Une clé retirée puis remise passe à la fin. Modifier ou retirer des clés existantes pendant un pairs ne pose pas de problème ; les clés ajoutées pendant la boucle peuvent ne pas être visitées.
coffre.list() (inventory.list()) est la table à trous typique : elle a une clé pour chaque emplacement qui contient quelque chose, et aucune pour les emplacements vides. ipairs s'arrêterait au premier emplacement vide ; pairs les voit tous :
-- ce que coffre.list() renvoie : seulement les emplacements occupés
local emplacements = {}
emplacements[1] = {name = "minecraft:iron_ingot", count = 64}
emplacements[2] = {name = "minecraft:iron_ingot", count = 12}
emplacements[5] = {name = "create:andesite_alloy", count = 30}
print("#emplacements = " .. #emplacements)
for n, objet in pairs(emplacements) do
print(n, objet.count, objet.name)
end#emplacements = 2 1 64 minecraft:iron_ingot 2 12 minecraft:iron_ingot 5 30 create:andesite_alloy
Chaque objet a aussi un champ display, son nom tel qu'affiché en jeu. table.keys(t) renvoie la liste des clés d'une table, pratique pour les trier avant de les afficher.
Les fiches : une liste de machines
Une fiche est une table aux champs nommés qui décrit une chose. Une liste de fiches décrit plusieurs choses du même genre, et les boucles qui la parcourent se lisent comme des phrases :
local machines = {
{nom = "Broyeur A", rpm = 128, surcharge = false},
{nom = "Broyeur B", rpm = 0, surcharge = true},
{nom = "Presse", rpm = 64, surcharge = false},
}
for _, m in ipairs(machines) do
local etat = "en marche"
if m.surcharge then
etat = "SURCHARGE"
elseif m.rpm == 0 then
etat = "à l'arrêt"
end
print(string.format("%-10s %4d tr/min %s", m.nom, m.rpm, etat))
endBroyeur A 128 tr/min en marche Broyeur B 0 tr/min SURCHARGE Presse 64 tr/min en marche
Avec de vraies machines, chaque fiche serait remplie depuis les blocs eux-mêmes (@kinetic.speed, kinetic.overstressed()) et la liste affichée sur un moniteur : voir le tableau de bord d'usine.
Les objets : des fiches avec des méthodes
Mettez des fonctions dans une fiche et elle devient un objet : des données plus les méthodes qui travaillent dessus, appelées avec :. Pour fabriquer beaucoup d'objets du même genre, écrivez les méthodes une fois dans une table, et une fonction qui construit chaque objet et y recopie les méthodes. (Brass n'a pas de métatables : chaque objet porte donc ses propres références vers les méthodes ; les fonctions elles-mêmes sont partagées, pas dupliquées.)
local Reservoir = {}
function Reservoir:remplir(mb)
self.quantite = math.min(self.capacite, self.quantite + mb)
end
function Reservoir:pourcentage()
return math.floor(self.quantite * 100 / self.capacite)
end
local function nouveau_reservoir(fluide, capacite)
local reservoir = {fluide = fluide, quantite = 0, capacite = capacite}
for nom, methode in pairs(Reservoir) do
reservoir[nom] = methode
end
return reservoir
end
local lave = nouveau_reservoir("lave", 8000)
local eau = nouveau_reservoir("eau", 4000)
lave:remplir(6000)
lave:remplir(1000)
eau:remplir(5000)
print(lave.fluide .. " : " .. lave:pourcentage() .. " %")
print(eau.fluide .. " : " .. eau:pourcentage() .. " %")lave : 87 % eau : 100 %
Les ensembles
Un ensemble répond à une seule question : cette valeur en fait-elle partie ? Utilisez les valeurs comme clés et true comme valeur. Vérifier ne demande qu'une recherche, autorises[nom], quelle que soit la taille de l'ensemble.
local autorises = {Steve = true, Alex = true}
autorises["Notch"] = true -- ajout
autorises.Alex = nil -- retrait
for _, joueur in ipairs({"Steve", "Alex", "Herobrine", "Notch"}) do
if autorises[joueur] then
print(joueur .. " : bienvenue")
else
print(joueur .. " : accès refusé")
end
endSteve : bienvenue Alex : accès refusé Herobrine : accès refusé Notch : bienvenue
table.contains(liste, valeur) donne la même réponse sur une liste, mais elle passe en revue chaque élément : très bien pour dix noms, plus lent pour mille. Un ensemble est l'outil d'une porte qui ne laisse entrer que certains joueurs.
Compter et regrouper
Pour compter combien de fois chaque valeur apparaît, utilisez la valeur comme clé et ajoutez un à chaque fois. Le (compte[k] or 0) donne 0 la première fois qu'une clé est vue :
local passages = {"Ligne rouge", "Ligne bleue", "Ligne rouge", "Fret", "Ligne rouge", "Ligne bleue"}
local compte = {}
for _, train in ipairs(passages) do
compte[train] = (compte[train] or 0) + 1
end
for train, n in pairs(compte) do
print(train, n)
endLigne rouge 3 Ligne bleue 2 Fret 1
Regrouper va un cran plus loin : chaque clé contient une table qui rassemble tout ce qui lui appartient. Ici, le contenu d'un coffre regroupé par mod, d'après la partie de l'identifiant avant le : :
-- ce que coffre.list() pourrait renvoyer
local objets = {
{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 par_mod = {}
for _, objet in pairs(objets) do
local morceaux = objet.name:split(":") -- {"minecraft", "iron_ingot"}
local mod = morceaux[1]
if par_mod[mod] == nil then
par_mod[mod] = {total = 0, sortes = {}}
end
local groupe = par_mod[mod]
groupe.total = groupe.total + objet.count
table.insert(groupe.sortes, morceaux[2])
end
for mod, groupe in pairs(par_mod) do
print(mod .. " : " .. groupe.total .. " (" .. table.concat(groupe.sortes, ", ") .. ")")
endminecraft : 76 (iron_ingot, coal) create : 39 (andesite_alloy, brass_ingot) computingages : 4 (copper_wire)
La boucle utilise pairs plutôt qu'ipairs parce que le vrai coffre.list() saute les emplacements vides. string.split coupe l'identifiant en deux (voir Chaînes).
Ce que coûtent les tables
Chaque ordinateur a une mémoire comptée en cellules, de 2 048 pour un Calculateur à tubes à 1 048 576 pour un Ordinateur moderne (131 072 pour un Micro-ordinateur). C'est dans les tables que la plupart part :
| chose | cellules |
|---|---|
| une table | 4 |
| chaque entrée d'une table (clé et valeur) | 2 |
| une chaîne | 1, plus 1 par tranche de 8 caractères |
Une liste de 1 000 mesures de capteur coûte environ 2 004 cellules : rien pour un Micro-ordinateur, toute la mémoire d'un Calculateur à tubes. La mémoire est rendue d'elle-même quand plus rien ne fait référence à une table (la locale d'une fonction qui s'est terminée, un champ ou une variable remis à nil). Un programme qui garde plus que ce que l'ordinateur peut contenir s'arrête avec out of memory.
Ne gardez donc que l'utile : un journal glissant plutôt qu'un journal sans fin, et des compteurs plutôt que la liste complète des évènements. os.memory indique la mémoire utilisée ; Performances explique en détail le budget d'instructions et de cellules.