Create: Computing AgesDoc Brass
Le langage Brass

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 = ...}.

Brass
local chaine = {"presse", "mixeur", "scie"}        -- une liste
local presse = {genre = "presse", rpm = 64}        -- une fiche
print(chaine[1], #chaine, presse.rpm)
Écran
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 :

appeleffet
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
Brass
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])
Écran
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 :

Brass
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, " "))
Écran
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 :

Brass
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)
Écran
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 :

Brass
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, ", "))
Écran
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é.

Brass
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)
end
Écran
40  12  nil
minecraft:iron_ingot    128
create:brass_ingot  40
  • t.nom est un raccourci de t["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.objet est la clé "objet", alors que t[objet] utilise la valeur de la variable objet.
  • Lire une clé absente donne nil. Donner la valeur nil à une clé la retire de la table.
  • t[nil] = 1 est une erreur : table index is nil.
  • 1 et "1" sont deux clés différentes : un nombre lu dans un texte doit passer par tonumber avant de trouver la clé numérique. 1 et 1.0 sont 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é :

Brass
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])
Écran
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 :

Brass
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)
Écran
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.

Brass
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({} == {})
Écran
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.copy fait 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 :
Brass
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])
Écran
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 :

boucleparcourtà utiliser pour
for i, v in ipairs(t) dot[1], t[2]... jusqu'au premier nilles listes
for v in t doles mêmes valeurs, sans la positionles listes, quand la position ne compte pas
for k, v in pairs(t) dotoutes les cléstout 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 :

Brass
-- 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
Écran
#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 :

Brass
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))
end
Écran
Broyeur 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.)

Brass
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() .. " %")
Écran
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.

Brass
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
end
Écran
Steve : 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 :

Brass
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)
end
Écran
Ligne 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 : :

Brass
-- 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, ", ") .. ")")
end
Écran
minecraft : 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 :

chosecellules
une table4
chaque entrée d'une table (clé et valeur)2
une chaîne1, 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.