Create: Computing AgesDoc Brass
Le langage Brass

Programmes en plusieurs fichiers

Découper un programme en modules avec import, ranger un projet en dossiers, le lancer depuis startup, et partager du code entre ordinateurs.

Un programme qui pilote toute une usine atteint vite des centaines de lignes : lire les coffres à objet renforcés, dessiner l'écran, parler aux autres ordinateurs. Passé une certaine taille, un seul fichier devient difficile à lire et à modifier. Brass permet de le découper : chaque fichier fait un travail, et import rassemble les morceaux. Ces mêmes morceaux servent ensuite à vos autres programmes : écrivez une barre de progression une fois, utilisez-la partout.

Un premier module

Un module est un fichier ordinaire du support qui se termine par return et une valeur, presque toujours une table de fonctions. Voici une petite bibliothèque d'outils pour le texte, enregistrée sous lib/texte (edit lib/texte crée le dossier et le fichier) :

lib/texte
local M = {}

-- "Fer" -> "Fer       " (coupé ou complété par des espaces jusqu'à la largeur)
function M.caler(texte, largeur)
  texte = tostring(texte)
  if #texte >= largeur then
    return texte:sub(1, largeur)
  end
  return texte .. string.rep(" ", largeur - #texte)
end

-- 1234567 -> "1 234 567"
function M.milliers(n)
  if n < 0 then
    return "-" .. M.milliers(-n)
  end
  local chiffres = tostring(math.floor(n))
  local resultat = ""
  while #chiffres > 3 do
    resultat = " " .. chiffres:sub(-3) .. resultat
    chiffres = chiffres:sub(1, -4)
  end
  return chiffres .. resultat
end

return M

Un programme s'en sert avec import, qui exécute le fichier et renvoie la table qu'il renvoie :

stock
local texte = import "lib/texte"

print(texte.caler("Lingot de fer", 14) .. texte.milliers(1234567))
print(texte.caler("Or", 14) .. texte.milliers(87))

Les noms sont à vous : texte pourrait s'appeler t ou txt. Par habitude, un module construit une table nommée M (pour module) et la renvoie à la fin.

import et require

import(chemin) exécute le fichier situé à chemin et renvoie ce que ce fichier renvoie. require est la même fonction, sous le nom que connaissent les joueurs de ComputerCraft. Comme un appel avec une seule chaîne n'a pas besoin de parenthèses, la forme habituelle est :

Brass
local texte = import "lib/texte"

Seul sur sa ligne, import "lib/preparation" exécute un fichier et ignore ce qu'il renvoie : pratique pour un fichier qui ne fait que définir des fonctions globales ou préparer l'écran.

Un fichier sans return donne nil. Un module peut renvoyer n'importe quoi (un nombre, une chaîne, une fonction), mais une table est ce qu'il vous faut dans presque tous les cas.

Où le fichier est cherché

Le chemin part **du dossier du fichier qui appelle import**, pas du dossier où vous avez tapé la commande. Un fichier de lib qui importe "texte" obtient lib/texte.

chemindepuis apps/stock/main, désigne
"ecran"apps/stock/ecran (même dossier)
"parties/barres"apps/stock/parties/barres
"../porte/main"apps/porte/main (.. remonte d'un dossier)
"/lib/texte"lib/texte (un / au début part de la racine du support)

Les lignes tapées à l'invite brass importent depuis le dossier courant (celui choisi avec cd). Les noms de fichier n'ont pas d'extension : import "lib/texte", pas "lib/texte.lua".

Quand le fichier ne peut pas être chargé, le programme s'arrête avec une erreur qui cite le chemin tel que vous l'avez écrit :

messagesens
cannot import 'lib/txt': no such fileaucun fichier à ce chemin (vérifiez l'orthographe, et le dossier du fichier qui importe)
cannot import 'lib': it is a folderle chemin désigne un dossier
cannot import '../../x': no folder above the roottrop de ..
cannot import 'ma lib': invalid file name 'ma lib'les noms n'utilisent que des lettres, des chiffres, _, . et -
no storage mediuml'ordinateur n'a pas de support

Un module s'exécute une fois par programme

Le premier import d'un fichier l'exécute. Tous les import suivants du même fichier, depuis n'importe quel fichier du programme, renvoient la même valeur sans l'exécuter de nouveau. Pour le voir d'un coup, ce test écrit un tout petit module avec fs.write, puis l'importe deux fois :

Brass
fs.write("lib/compteur", [[
print("lib/compteur s'exécute")
local M = {total = 0}
function M.ajouter()
  M.total = M.total + 1
end
return M
]])

local a = import "lib/compteur"
local b = import "lib/compteur"
a.ajouter()
b.ajouter()
print(a == b, a.total)
Écran
lib/compteur s'exécute
true    2

Le fichier s'est exécuté une fois, et a et b sont la même table : un module est un bon endroit pour un état partagé par tous les fichiers d'un programme (un cache de mesures, les réglages en cours).

Le souvenir de ce qui a été importé dure jusqu'à la fin du programme. Le lancement suivant importe tout à nouveau : après avoir modifié un module, relancez simplement le programme, pas besoin de redémarrer. À l'invite brass, chaque ligne est son propre petit programme, donc un import y exécute le fichier à chaque fois.

Ce qu'un module partage, et ce qu'il garde

  • Les **variables et fonctions local** au niveau du fichier d'un module sont à lui. Aucun autre fichier ne les voit, même avec le même nom : deux modules peuvent avoir chacun leur local function dessiner() sans problème.
  • Les variables et fonctions globales (déclarées sans local) sont partagées par tous les fichiers du programme.
Brass
fs.write("lib/partage", [[
local secret = "seulement dans lib/partage"
nom_ferme = "Ferme à fer"
function crier(message)
  print(message:upper())
end
]])

import "lib/partage"
print(secret)
print(nom_ferme)
crier("bonjour")
Écran
nil
Ferme à fer
BONJOUR

Gardez tout en local et renvoyez dans la table du module ce dont les autres fichiers ont besoin. Les globales semblent pratiques, mais deux modules qui choisissent le même nom global écrasent la valeur l'un de l'autre, et rien ne vous prévient.

Dans un module, les fonctions peuvent utiliser les locales du niveau du fichier (comme M plus haut) : la règle qui interdit à une fonction d'utiliser les locales d'une fonction englobante ne s'applique pas au niveau du fichier (voir Fonctions).

Les imports circulaires

Si lib/a importe lib/b pendant que lib/b importe lib/a, aucun des deux ne peut finir en premier. Brass arrête le programme au lieu de tourner en rond pour toujours :

Brass
fs.write("lib/a", 'import "b"\nreturn {}')
fs.write("lib/b", 'import "a"\nreturn {}')
import "lib/a"
Écran
lib/b:1: circular import: 'lib/a' is still being im
ported

Le remède est de casser le cercle : déplacez ce dont les deux fichiers ont besoin dans un troisième module qui n'importe aucun des deux, ou passez la valeur en paramètre d'une fonction au lieu de l'importer.

Les erreurs dans un module

Une erreur dans un module porte le nom et la ligne du module lui-même, pour que vous sachiez quel fichier ouvrir :

Brass
fs.write("lib/casse", "local x = \nreturn x")
import "lib/casse"
Écran
lib/casse:2: unexpected symbol near 'return'

Une erreur de compilation dans un module apparaît au moment où il est importé, pas au démarrage du programme principal : tout ce qui précède la ligne import s'est déjà exécuté. Un module dont l'exécution a échoué est exécuté de nouveau au prochain import.

Un module avec des réglages

Un fichier de réglages écrit en Brass

Un fichier qui renvoie simplement une table est une façon claire et sûre de séparer les réglages d'un programme de son code. Un joueur peut changer les réglages sans lire le programme, et les commentaires expliquent chaque valeur :

config
return {
  titre = "STOCK DE LA FERME À FER",
  intervalle = 5,           -- secondes entre deux lectures
  cote_alarme = "top",      -- lampe allumée quand un stock est bas
  objets = {
    {nom = "Lingot de fer", id = "minecraft:iron_ingot", max = 2000},
    {nom = "Pépite de fer", id = "minecraft:iron_nugget", max = 1000},
    {nom = "Fer concassé", id = "create:crushed_raw_iron", max = 500},
  },
}
Brass
local config = import "/config"
print(config.titre)

Contrairement à un fichier texte cle = valeur (voir Travailler avec du texte), celui-ci peut contenir des listes et des tables dans des tables, et n'a besoin d'aucun lecteur. Le prix : une faute de frappe y est une erreur de compilation, signalée config:5: ....

Des valeurs par défaut que le programme peut changer

Puisque chaque fichier reçoit la même table, un module peut proposer des valeurs par défaut que le programme principal change une fois au démarrage :

lib/alarme
local M = {cote = "top", niveau = 15}

function M.sonner()
  rs.set(M.cote, M.niveau)
end

function M.arreter()
  rs.set(M.cote, 0)
end

return M
main
local alarme = import "/lib/alarme"
alarme.cote = "back"      -- cet ordinateur a sa lampe à l'arrière
alarme.sonner()

Plusieurs objets à partir d'un module

Quand il vous faut plusieurs exemplaires d'une même chose (trois lampes, deux presses), le module fournit une fonction qui construit une nouvelle table pour chacun. En Lua, on garderait les données de chaque objet dans une fermeture (closure) ; les fonctions de Brass ne peuvent pas capturer les locales d'une autre fonction, alors les données vont dans la table, et les fonctions reçoivent la table en self :

lib/lampe
local Lampe = {}

local function allumer(self)
  rs.set(self.cote, self.niveau)
  self.allumee = true
end

local function eteindre(self)
  rs.set(self.cote, 0)
  self.allumee = false
end

local function basculer(self)
  if self.allumee then
    eteindre(self)
  else
    allumer(self)
  end
end

function Lampe.nouvelle(cote, niveau)
  return {cote = cote, niveau = niveau or 15, allumee = false, allumer = allumer, eteindre = eteindre, basculer = basculer}
end

return Lampe
main
local Lampe = import "/lib/lampe"

local alarme = Lampe.nouvelle("top")
local balise = Lampe.nouvelle("back", 7)
alarme:allumer()
balise:basculer()

alarme:allumer() est un raccourci pour alarme.allumer(alarme) : les deux-points passent la table en premier argument, self. C'est pour vos propres objets. Les appareils que renvoie peripheral.wrap sont différents : leurs fonctions s'appellent avec un point, lampe.set(15) (voir Périphériques).

Ranger un projet en dossiers

Une organisation qui grandit bien : les bibliothèques partagées dans lib, un dossier par programme dans apps, les réglages à la racine, et un tout petit startup.

/startup            lance le programme de cet ordinateur
/config             les réglages de cet ordinateur
/lib/texte          outils pour le texte (caler, milliers)
/lib/ui             outils de dessin (barres, lignes)
/apps/stock/main    l'écran des stocks
/apps/porte/main    le contrôle de la porte

Les commandes du shell comprennent les chemins et les dossiers :

Terminal
> mkdir lib
> edit lib/texte
> edit apps/stock
> cd apps/stock
/apps/stock> main

edit apps/stock ouvre l'éditeur avec l'arborescence du dossier à gauche : cliquez sur un fichier pour l'ouvrir, plusieurs restent ouverts côte à côte. Le shell et l'éditeur donne les détails.

Quelques habitudes aident :

  • Importez les bibliothèques partagées avec un chemin depuis la racine, import "/lib/texte" : la ligne est la même dans tous les fichiers, où qu'ils soient.
  • Importez les fichiers d'un même programme avec un chemin relatif, import "ecran" : le dossier peut être renommé ou copié sans changer le code.
  • Noms de fichiers et de dossiers : lettres, chiffres, _, . et -, 32 caractères au plus ; un chemin tient en 128 caractères ; un support contient 1024 fichiers et dossiers.

Démarrer depuis startup

Au démarrage, un ordinateur exécute le fichier nommé startup à la racine de son support. Réduisez-le à une ligne qui importe le programme principal :

startup
import "/apps/stock/main"

import est le moyen pour un programme d'en lancer un autre : le programme principal s'exécute à l'intérieur, et comme il boucle en général pour toujours, startup ne se termine jamais. S'il se termine, l'invite revient.

Pour choisir le programme sans modifier startup, lisez son nom dans un fichier :

startup
local nom = fs.read("/autorun")    -- par exemple "stock", écrit avec : edit autorun
if nom ~= nil then
  import("/apps/" .. nom:trim() .. "/main")
end

Un exemple complet

L'écran des stocks de l'organisation ci-dessus, en quatre fichiers : deux bibliothèques, les réglages (le fichier config montré plus haut) et le programme. Il trouve un coffre à objet renforcé ou un coffre à côté de l'ordinateur (ou sur son Câble de données), et affiche une barre par objet, rouge quand le stock est bas.

lib/texte
local M = {}

function M.caler(texte, largeur)
  texte = tostring(texte)
  if #texte >= largeur then
    return texte:sub(1, largeur)
  end
  return texte .. string.rep(" ", largeur - #texte)
end

function M.milliers(n)
  if n < 0 then
    return "-" .. M.milliers(-n)
  end
  local chiffres = tostring(math.floor(n))
  local resultat = ""
  while #chiffres > 3 do
    resultat = " " .. chiffres:sub(-3) .. resultat
    chiffres = chiffres:sub(1, -4)
  end
  return chiffres .. resultat
end

return M
lib/ui
local texte = import "texte"   -- le même dossier : lib/texte

local M = {}

-- une barre horizontale : la partie pleine en couleur, le reste en gris
function M.barre(x, y, largeur, part, couleur)
  local pleine = math.floor(largeur * math.max(0, math.min(1, part)) + 0.5)
  term.set_cursor(x, y)
  term.set_bg(couleur)
  term.write(string.rep(" ", pleine))
  term.set_bg(term.colors.gray)
  term.write(string.rep(" ", largeur - pleine))
  term.set_bg(term.colors.black)
end

-- une ligne du tableau : nom, quantité, barre
function M.ligne(y, nom, nombre, max)
  term.set_cursor(1, y)
  term.set_fg(term.colors.white)
  term.write(texte.caler(nom, 14) .. texte.caler(texte.milliers(nombre), 9))
  local couleur = term.colors.lime
  if nombre < max * 0.2 then
    couleur = term.colors.red
  end
  M.barre(24, y, 26, nombre / max, couleur)
end

return M
apps/stock/main
local config = import "/config"
local ui = import "/lib/ui"

local coffre = peripheral.find("inventory")
if coffre == nil then
  error("pas d'inventaire à côté de l'ordinateur ni sur son câble")
end

while true do
  term.set_bg(term.colors.black)
  term.clear()
  term.set_cursor(1, 1)
  term.set_fg(term.colors.yellow)
  term.write(config.titre)
  local bas = false
  for i, objet in ipairs(config.objets) do
    local nombre = coffre.count(objet.id)
    ui.ligne(2 + i, objet.nom, nombre, objet.max)
    if nombre < objet.max * 0.2 then
      bas = true
    end
  end
  rs.set(config.cote_alarme, bas)
  sleep(config.intervalle)
end
startup
import "/apps/stock/main"

Le programme principal dit quoi afficher ; lib/ui sait comment le dessiner ; config dit quels objets. Pour surveiller un second coffre renforcé sur un autre ordinateur, copiez les quatre fichiers et ne changez que config.

Partager du code entre ordinateurs

Les fichiers vivent sur le support, pas dans l'ordinateur. Trois façons d'amener une bibliothèque ou un programme sur un autre ordinateur :

  • Emporter le support. Accroupi + clic droit sur l'ordinateur à main vide éjecte son support ; clic droit avec sur un autre ordinateur. Tous les fichiers suivent. Un ordinateur lit les supports de son âge et des âges précédents (Paquet de cartes perforées, Bobine de bande magnétique, Disquette, SSD), voir Les ordinateurs.
  • Copier-coller. Ouvrez le fichier dans l'éditeur, Ctrl+A puis Ctrl+C ; ouvrez un fichier sur l'autre ordinateur et collez avec Ctrl+V. C'est le moyen le plus simple d'entrer dans un Microcontrôleur, dont la mémoire est soudée et qui ne prend aucun support. Ça marche aussi depuis ce site : le bouton Copier d'un bloc de code, puis Ctrl+V dans l'éditeur.
  • L'envoyer par le réseau. À partir du Mini-ordinateur, des ordinateurs reliés par Câble de données (ou par radio, à partir du Micro-ordinateur) peuvent s'envoyer du texte. Deux tout petits programmes copient un fichier :
envoyer
-- envoyer <fichier> <numéro d'ordinateur> : envoie un fichier à un autre ordinateur
local chemin, cible = arg[1], tonumber(arg[2])
local texte = assert(fs.read(chemin), "pas de fichier " .. tostring(chemin))
if net.send(cible, {chemin = chemin, texte = texte}, "fichiers") then
  print("envoyé : " .. #texte .. " caractères")
else
  print("ordinateur " .. tostring(cible) .. " injoignable")
end
recevoir
-- recevoir : écrit chaque fichier envoyé à cet ordinateur
print("en attente de fichiers... (ordinateur " .. os.id() .. ")")
while true do
  local m = os.pull_event("message")
  if m.channel == "fichiers" and type(m.data) == "table" then
    fs.write(m.data.chemin, m.data.texte)
    print("reçu " .. m.data.chemin .. " de " .. m.sender)
  end
end

Lancez recevoir sur un ordinateur, puis envoyer lib/texte 12 sur l'autre (12 étant le numéro qu'affiche le receveur). Un message transporte jusqu'à 32768 caractères ; Réseaux explique le reste.

Voir aussi