Create: Computing AgesDoc Brass
Le langage Brass

Fonctions

Nommer un morceau de code pour le réutiliser : paramètres, valeur renvoyée, méthodes, récursivité, et la règle de portée qui distingue Brass de Lua.

Une fonction est un morceau de code qui porte un nom, et qu'on peut exécuter (appeler) autant de fois qu'on veut, avec des valeurs différentes à chaque fois. Vous appelez déjà des fonctions : print, sleep, rs.set. Écrire les vôtres permet de nommer une étape du programme (« compter le fer », « ouvrir la porte »), de l'écrire une fois et de s'en servir partout.

Brass
local function piles(objets)
  return objets // 64
end
print(piles(200))
print(piles(1728))
Écran
3
27

Cette page suppose que vous connaissez les valeurs et les variables et les conditions et les boucles. Si vous venez de Lua, lisez attentivement la règle de portée : c'est le seul endroit où Brass se comporte autrement.

Définir et appeler

Brass
local function nom(parametre1, parametre2)
  -- le corps : le code exécuté à chaque appel
  return parametre1 + parametre2
end

local function crée la fonction et la range dans une variable locale. L'appeler, c'est écrire son nom suivi des valeurs entre parenthèses : nom(3, 4). Les parenthèses sont obligatoires, même sans valeur : ouvrir_porte().

Il existe plusieurs façons d'écrire une fonction, qui fabriquent toutes la même chose :

écriturerangée dans
local function f() ... endune variable locale f (le choix habituel)
local f = function() ... endpareil, mais la fonction ne peut pas s'appeler elle-même par son nom
function f() ... endune variable globale f, vue depuis tous les fichiers jusqu'au redémarrage de l'ordinateur
function t.nom() ... endle champ nom de la table t
function t:nom() ... endle champ nom de t, comme méthode (voir les méthodes)

Définir une fonction est une instruction, comme une affectation : la fonction existe à partir du moment où cette ligne s'est exécutée. L'appeler sur une ligne située avant sa définition échoue avec attempt to call a nil value (global 'ouvrir_porte'). Mettez vos fonctions en haut du fichier et le code principal en bas.

Paramètres

Les noms entre parenthèses sont les paramètres : des variables locales de la fonction, remplies avec les valeurs données à l'appel (les arguments), dans l'ordre. Un argument manquant laisse son paramètre à nil ; les arguments en trop sont ignorés.

Brass
local function rapport(machine, rpm, unite)
  print(machine, rpm, unite)
end
rapport("presse", 64)
rapport("scie", 128, "tr/min", "en trop")
Écran
presse  64  nil
scie    128 tr/min

Les paramètres facultatifs deviennent donc faciles : donnez-leur une valeur par défaut au début de la fonction avec or.

Brass
local function allumer_lampe(cote, force)
  force = force or 15   -- pleine puissance si rien n'est donné
  rs.set(cote, force)
  print("lampe " .. cote .. " à " .. force)
end
allumer_lampe("top")
allumer_lampe("left", 7)
Écran
lampe top à 15
lampe left à 7

L'astuce du or remplace false aussi bien que nil. Pour un paramètre où false est un vrai choix, écrivez plutôt if actif == nil then actif = true end.

Une fonction Brass prend une liste fixe de paramètres : ... (un nombre quelconque d'arguments) est une erreur de compilation, variable arguments ('...') are not supported. Pour passer « autant de choses qu'on veut », passez une table :

Brass
local function total(quantites)
  local somme = 0
  for _, n in ipairs(quantites) do
    somme = somme + n
  end
  return somme
end
print(total({64, 64, 12}))
Écran
140

Renvoyer une valeur

return valeur termine la fonction et rend la valeur à l'appelant : l'appel prend la place de cette valeur. Une fonction qui se termine sans return, ou avec un return seul, donne nil.

Une fonction Brass renvoie une seule valeur. return a, b ne compile pas : a function returns a single value (return a table instead). Quand vous avez plusieurs choses à rendre, mettez-les dans une table et lisez ses champs :

Brass
local function decouper(objets)
  return {piles = objets // 64, reste = objets % 64}
end
local resultat = decouper(200)
print(resultat.piles .. " piles et " .. resultat.reste .. " objets")
Écran
3 piles et 8 objets

return doit être la dernière instruction de son bloc. Pour quitter une fonction plus tôt, placez-le dans un if ; ce style « garde-fou » laisse le cas normal sans indentation :

Brass
local function moyenne(mesures)
  if #mesures == 0 then
    return nil           -- rien à moyenner
  end
  local somme = 0
  for _, m in ipairs(mesures) do
    somme = somme + m
  end
  return somme / #mesures
end
print(moyenne({60, 64, 68}), moyenne({}))
Écran
64  nil

Les fonctions sont des valeurs

Une fonction est une valeur, comme un nombre ou une chaîne. On peut la ranger dans une variable ou une table, et la passer à une autre fonction. Une fonction écrite sans nom, function(a, b) ... end, est une fonction anonyme : pratique quand on n'en a besoin qu'à un seul endroit.

Le cas le plus connu est table.sort, qui prend une fonction disant si a doit passer avant b :

Brass
local machines = {
  {nom = "presse", charge = 512},
  {nom = "ventilateur", charge = 128},
  {nom = "perceuse", charge = 1024},
}
table.sort(machines, function(a, b) return a.charge > b.charge end)
for _, m in ipairs(machines) do
  print(m.nom, m.charge)
end
Écran
perceuse    1024
presse  512
ventilateur 128

Un aiguilleur de commandes

Des fonctions rangées dans une table, sous le nom d'une commande, forment une table d'aiguillage : le programme cherche la commande et appelle ce qu'il trouve, au lieu d'une longue suite de if ... elseif. Ajouter une commande, c'est ajouter une fonction.

Brass
local porte_ouverte = false
local commandes = {}

function commandes.ouvrir()
  porte_ouverte = true
  rs.set("back", true)
  return "porte ouverte"
end

function commandes.fermer()
  porte_ouverte = false
  rs.set("back", false)
  return "porte fermée"
end

function commandes.etat()
  if porte_ouverte then return "la porte est ouverte" end
  return "la porte est fermée"
end

function commandes.aide()
  local noms = table.keys(commandes)
  table.sort(noms)
  return "commandes : " .. table.concat(noms, ", ")
end

local function executer(ligne)
  local action = commandes[ligne]
  if action == nil then
    return "commande inconnue '" .. ligne .. "', essayez aide"
  end
  return action()
end

for _, tape in ipairs({"aide", "ouvrir", "etat", "exploser"}) do
  print("> " .. tape)
  print(executer(tape))
end
Écran
> aide
commandes : aide, etat, fermer, ouvrir
> ouvrir
porte ouverte
> etat
la porte est ouverte
> exploser
commande inconnue 'exploser', essayez aide

Dans le vrai programme, c'est le joueur qui tape les commandes : remplacez la dernière boucle par une boucle principale qui les lit.

Brass
while true do
  write("> ")
  print(executer(read()))
end

Les méthodes : : et self

Une table peut contenir à la fois des données et les fonctions qui travaillent dessus. Une telle fonction est une méthode, et Brass a un raccourci pour elle : function porte:basculer() revient à function porte.basculer(self), et l'appel porte:basculer() revient à porte.basculer(porte). Le deux-points passe la table elle-même comme premier paramètre caché, nommé self.

Brass
local porte = {cote = "back", ouverte = false}

function porte:basculer()
  self.ouverte = not self.ouverte
  rs.set(self.cote, self.ouverte)
  if self.ouverte then return "porte ouverte" end
  return "porte fermée"
end

print(porte:basculer())
print(porte:basculer())
Écran
porte ouverte
porte fermée

Appeler une méthode avec un point, porte.basculer(), ne passe rien comme self et échoue dans la méthode avec attempt to index a nil value (local 'self'). Quand vous voyez ce message, cherchez un . qui devrait être un :.

Les méthodes font des objets : des tables qui portent leur propre état. Tables montre comment fabriquer beaucoup d'objets du même genre.

La récursivité

Une fonction peut s'appeler elle-même : c'est la récursivité. Elle convient aux choses qui contiennent des choses plus petites du même genre, comme des caisses dans des caisses :

Brass
local function compter_objets(caisse)
  local total = 0
  for _, chose in ipairs(caisse) do
    if type(chose) == "table" then
      total = total + compter_objets(chose)   -- une caisse dans la caisse
    else
      total = total + chose
    end
  end
  return total
end
print(compter_objets({64, 32, {16, 16, {8}}, 4}))
Écran
140

Une local function peut s'appeler elle-même par son nom ; local f = function() ... end ne le peut pas (à l'intérieur, f n'est pas encore déclarée et désigne une globale f, qui vaut nil).

La limite de profondeur des appels

Chaque appel qui n'est pas encore terminé occupe une place dans la pile d'appels, et la pile contient 200 appels (le programme principal compte pour un). Aller plus loin arrête le programme avec stack overflow :

Brass
local function creuser(profondeur)
  if profondeur == 0 then return "bedrock" end
  return creuser(profondeur - 1)
end
print(creuser(150))
print(creuser(300))
Écran
bedrock
snippet:3: stack overflow

Brass n'a pas d'appels terminaux : return creuser(profondeur - 1) occupe quand même une place, contrairement à Lua. Chaque appel en attente coûte aussi 8 cellules de mémoire. Pour un travail qui peut aller loin (remplir une zone bloc par bloc, suivre une longue chaîne), utilisez une boucle et une liste des choses qui restent à faire plutôt que la récursivité.

La règle de portée

C'est la règle qui distingue Brass de Lua : elle mérite une lecture attentive.

  1. Les locales de fichier, les variables local écrites au premier niveau d'un fichier (hors de toute fonction), sont partagées par toutes les fonctions de ce fichier : chacune peut les lire et les modifier.
  2. Les locales d'une fonction, ses paramètres et les variables local déclarées dans son corps, ne sont visibles que dans ce corps. Pas dans les fonctions écrites à l'intérieur.
  3. Une fonction écrite dans une autre fonction et qui utilise une locale de la fonction extérieure ne compile pas : cannot capture local 'compte' of an enclosing function, précédé du fichier et de la ligne.
Brass
local function commencer_comptage()
  local compte = 0
  local function ajouter_train()
    compte = compte + 1   -- erreur : compte appartient à commencer_comptage
  end
  ajouter_train()
end

Une fonction imbriquée peut tout de même utiliser ses propres paramètres et locales, les locales de fichier et les globales. Ceci est correct :

Brass
local unite = "tr/min"      -- locale de fichier : toutes les fonctions la voient

local function rapport(machines)
  local function ligne(m)   -- une fonction imbriquée qui utilise son paramètre et une locale de fichier
    return m.nom .. " : " .. m.rpm .. " " .. unite
  end
  for _, m in ipairs(machines) do
    print(ligne(m))
  end
  return #machines
end

print(rapport({{nom = "presse", rpm = 64}, {nom = "ventilateur", rpm = 128}}) .. " machines")
Écran
presse : 64 tr/min
ventilateur : 128 tr/min
2 machines

Pourquoi cette règle ? Les locales d'une fonction vivent dans l'espace propre à cet appel, libéré dès que la fonction se termine. Sans fermetures, rien ne peut garder cet espace en vie dans votre dos : la mémoire reste facile à prévoir sur des machines qui comptent chaque cellule.

Là où elle se fait sentir, en pratique :

  • une fonction d'aide dans une autre fonction, qui utilise ses paramètres ;
  • un comparateur anonyme pour table.sort qui utilise une locale de la fonction autour ;
  • deux local function d'aide dans une fonction, qui s'appellent l'une l'autre (le nom de la première est aussi une locale) ;
  • une fonction créée dans une boucle, dans une fonction, qui utilise la variable de boucle.

L'ordre compte au niveau du fichier

Une fonction voit les locales de fichier déclarées au-dessus d'elle. Un nom déclaré plus bas n'est pas encore connu quand la fonction est écrite : il désigne alors une globale.

Brass
local function afficher()
  print("vitesse : " .. tostring(vitesse))
end
local vitesse = 64   -- déclarée après afficher : afficher ne la voit pas
afficher()
Écran
vitesse : nil

Déclarez l'état de votre programme en haut du fichier. Quand deux fonctions ont besoin l'une de l'autre, ou que vous aimez garder les petites fonctions d'aide en bas, déclarez le nom d'abord et remplissez-le plus tard :

Brass
local journal                -- déclarée ici, définie plus bas

local function ouvrir_porte()
  rs.set("back", true)
  journal("porte ouverte")
end

function journal(texte)      -- remplit la locale déclarée plus haut
  print("[porte] " .. texte)
end

ouvrir_porte()
Écran
[porte] porte ouverte

Du code qui demanderait une fermeture

En Lua, une fonction garde souvent un état dans les locales de la fonction qui l'a créée (une fermeture, ou closure). En Brass, rangez cet état là où une fonction a le droit d'aller. Trois façons, de la plus simple à la plus complète.

1. Passer la valeur en paramètre. Sortez la fonction d'aide au niveau du fichier et donnez-lui ce dont elle a besoin.

Brass
local function tout_afficher(machines, unite)
  local function ligne(m)
    return m.nom .. " : " .. m.rpm .. " " .. unite   -- erreur : unite appartient à tout_afficher
  end
  for _, m in ipairs(machines) do print(ligne(m)) end
end
Brass
local function ligne(m, unite)
  return m.nom .. " : " .. m.rpm .. " " .. unite
end

local function tout_afficher(machines, unite)
  for _, m in ipairs(machines) do
    print(ligne(m, unite))
  end
end

tout_afficher({{nom = "presse", rpm = 64}, {nom = "ventilateur", rpm = 128}}, "tr/min")
Écran
presse : 64 tr/min
ventilateur : 128 tr/min

2. Garder l'état au niveau du fichier. Un comparateur pour table.sort ne peut pas recevoir de paramètre en plus : le champ de tri va donc dans une locale de fichier que les deux fonctions voient.

Brass
local champ_tri = "nom"

local function par_champ(a, b)
  return a[champ_tri] < b[champ_tri]
end

local function trier_par(liste, champ)
  champ_tri = champ
  table.sort(liste, par_champ)
end

local machines = {
  {nom = "presse", charge = 512},
  {nom = "ventilateur", charge = 128},
  {nom = "perceuse", charge = 1024},
}
trier_par(machines, "charge")
for m in machines do write(m.nom, " ") end
print()
trier_par(machines, "nom")
for m in machines do write(m.nom, " ") end
print()
Écran
ventilateur presse perceuse
perceuse presse ventilateur

3. Garder l'état dans une table. Quand il faut plusieurs copies indépendantes de l'état (un compteur par porte, une fiche par machine), chaque copie est une table, et les fonctions la reçoivent. Avec des méthodes, la table arrive sous le nom self :

Brass
-- la façon Lua : ne compile pas en Brass
local function nouveau_compteur()
  local compte = 0
  return function()
    compte = compte + 1
    return compte
  end
end
Brass
local function nouveau_compteur(nom)
  local compteur = {nom = nom, compte = 0}
  function compteur:ajouter()
    self.compte = self.compte + 1   -- self, pas compteur : self est le paramètre d'ajouter
  end
  return compteur
end

local nord = nouveau_compteur("porte nord")
local sud = nouveau_compteur("porte sud")
nord:ajouter()
nord:ajouter()
sud:ajouter()
print(nord.nom .. " : " .. nord.compte)
print(sud.nom .. " : " .. sud.compte)
Écran
porte nord : 2
porte sud : 1

Écrire compteur.compte dans ajouter redonnerait l'erreur de capture : compteur est une locale de nouveau_compteur. Avec self, la méthode n'utilise que son propre paramètre.

Une machine à états pour une porte

Beaucoup d'automatismes sont des machines à états : l'appareil est dans un seul état à la fois, et chaque évènement le fait passer dans un autre état, ou pas. Une grande porte à pistons est fermee, ouverture, ouverte ou fermeture. Écrire les transitions dans une table les garde au même endroit, et une petite fonction les applique :

Brass
-- pour chaque état, l'évènement qui mène à l'état suivant
local transitions = {
  fermee    = {bouton = "ouverture"},
  ouverture = {fini = "ouverte"},
  ouverte   = {bouton = "fermeture", delai = "fermeture"},
  fermeture = {fini = "fermee", bloque = "ouverture"},
}
local etat = "fermee"

local function traiter(evenement)
  local suivant = transitions[etat][evenement]
  if suivant == nil then
    print(etat .. " : ignore " .. evenement)
    return
  end
  print(etat .. " -> " .. suivant)
  etat = suivant
end

traiter("bouton")
traiter("bouton")   -- déjà en ouverture : rien à faire
traiter("fini")
traiter("delai")
traiter("bloque")   -- un joueur gêne : on rouvre
traiter("fini")
Écran
fermee -> ouverture
ouverture : ignore bouton
ouverture -> ouverte
ouverte -> fermeture
fermeture -> ouverture
ouverture -> ouverte

etat est une locale de fichier : traiter peut donc la modifier. Dans le monde, les évènements viennent de os.pull_event : un évènement redstone pour le bouton, un évènement timer pour le délai (voir Évènements), et la porte règle ses sorties de redstone en entrant dans un nouvel état.

Des fonctions dans d'autres fichiers

Un programme peut être découpé en plusieurs fichiers : import exécute un autre fichier une fois et rend ce qu'il renvoie, en général une table de fonctions. Chaque fichier garde ses propres locales de fichier ; les globales sont partagées. Voir Modules.