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.
local function piles(objets)
return objets // 64
end
print(piles(200))
print(piles(1728))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
local function nom(parametre1, parametre2)
-- le corps : le code exécuté à chaque appel
return parametre1 + parametre2
endlocal 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 :
| écriture | rangée dans |
|---|---|
local function f() ... end | une variable locale f (le choix habituel) |
local f = function() ... end | pareil, mais la fonction ne peut pas s'appeler elle-même par son nom |
function f() ... end | une variable globale f, vue depuis tous les fichiers jusqu'au redémarrage de l'ordinateur |
function t.nom() ... end | le champ nom de la table t |
function t:nom() ... end | le 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.
local function rapport(machine, rpm, unite)
print(machine, rpm, unite)
end
rapport("presse", 64)
rapport("scie", 128, "tr/min", "en trop")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.
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)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 :
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}))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 :
local function decouper(objets)
return {piles = objets // 64, reste = objets % 64}
end
local resultat = decouper(200)
print(resultat.piles .. " piles et " .. resultat.reste .. " objets")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 :
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({}))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 :
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)
endperceuse 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.
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> 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.
while true do
write("> ")
print(executer(read()))
endLes 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.
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())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 :
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}))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 :
local function creuser(profondeur)
if profondeur == 0 then return "bedrock" end
return creuser(profondeur - 1)
end
print(creuser(150))
print(creuser(300))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.
- 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. - Les locales d'une fonction, ses paramètres et les variables
localdéclarées dans son corps, ne sont visibles que dans ce corps. Pas dans les fonctions écrites à l'intérieur. - 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.
local function commencer_comptage()
local compte = 0
local function ajouter_train()
compte = compte + 1 -- erreur : compte appartient à commencer_comptage
end
ajouter_train()
endUne 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 :
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")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.sortqui utilise une locale de la fonction autour ; - deux
local functiond'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.
local function afficher()
print("vitesse : " .. tostring(vitesse))
end
local vitesse = 64 -- déclarée après afficher : afficher ne la voit pas
afficher()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 :
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()[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.
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
endlocal 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")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.
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()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 :
-- la façon Lua : ne compile pas en Brass
local function nouveau_compteur()
local compte = 0
return function()
compte = compte + 1
return compte
end
endlocal 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)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 :
-- 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")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.