Let's Do It Romania - 24 Septembrie 2011



   

Lua de la Rio

   

Un limbaj de configurare simplu, puternic şi extensibil


   

Dacă aplicaţia pe care o scrieţi are nevoie de configurări complexe din partea utilizatorului, fişierele de genul INI-urilor din Windows nu vă vor ajuta îndeajuns. Veţi avea nevoie de un limbaj de configurare iar Lua ar putea fi soluţia cea mai bună.

Mircea Sârbu


În numărul 2 al revistei PC Report am publicat un mic articol în care prezentam un cod Pascal care permitea programelor în care era inclus (programe "gazdă") să-şi preia parametrii din fişiere text. Nimic spectaculos, dar foarte util în condiţiile în care, la acea vreme (1992), se lucra în linie de comandă.

Tot cam în acea perioada am scris o mică aplicaţie economică în C++. Trebuie să admit că aplicaţia era banală şi anostă şi, cu siguranţă, aş fi uitat-o cu desăvârşire dacă nu m-aş fi încăpăţânat să rezolv într-un mod cât mai elegant o problemă legată tot de configurarea programului. Cum toată lumea ştie, este contraindicat să "zideşti" în codul programului anumite valori care în decursul timpului se pot schimba. Un exemplu firesc îl reprezintă taxele şi impozitele. Joaca a început prin definirea unei clase speciale de date numerice - pe care, plin de inspiraţie, le-am numit "exterioare" - ce beneficiau de un constructor ceva mai complex. Acesta verifica mai întâi un fişier text în care căuta o pereche de gen nume=valoare, unde numele trebuia să coincidă cu numele variabilei interne. Dacă găsea aşa ceva, iniţializa variabila cu acea valoare, altfel folosea valoarea implicită "zidită" în cod. Prin supraîncărcarea operatorilor, variabila putea fi apoi folosită ca orice variabilă float.

Dacă tot am scris un mic parser, de ce să nu admit ca valoarea să poată fi exprimată prin expresii în care să poată interveni şi alte variabile "exterioare"? La urma urmei, nici modul de calcul nu este bătut în cuie. De pildă tva_redus = cota * tva. Astfel a luat naştere un mic interpretor. Şi, dacă tot am ajuns aici, de ce să nu permit definirea exterioară a unei metode algoritmice de calcul? Evaluarea condiţiilor plus câteva instrucţiuni de ramificare şi de ciclare sunt suficiente pentru a calcula, de pildă, impozitul pe salariu. Deja se punea problema definirii unui limbaj.

Şi astfel ajungem la noţiunea de limbaj de configurare.

Limbaje de configurare

Limbajele de configurare sunt, prin natura lor, limbaje interpretate. Ar fi absurd să îngropăm într-un cod executabil ceea ce am fi vrut să lăsăm deschis, la îndemâna utilizatorului final.

Pe de altă parte, limbajele de configurare sunt "limbaje înglobate" (embedded). E o exprimare uşor forţată, care vrea să spună că interpretorul limbajului este înglobat în programul căruia îi serveşte, program numit "gazdă" (host). Pe de altă parte, aceasta înseamnă că în limbajele de configurare nu există noţiunea de "program principal" (main), deoarece configurarea este condusă de programul gazdă.

Diferenţa între limbajele de configurare şi alte limbaje interpretate (de pildă cele de scripting) nu este foarte netă. De fapt, şi alte limbaje se pot folosi pentru configurare. Două exemple notorii sunt TCL şi Lisp. Cu toate acestea, există nişte cerinţe speciale pe care un limbaj de configurare veritabil trebuie să le îndeplinească:

  • Să aibă o sintaxă clară şi simplă -- Este evident că limbajele ce configurare nu sunt destinate programatorilor, ci utilizatorilor programului gazdă, a căror expertiză este de regulă concentrată în domeniul acestei aplicaţii. Nu s-ar putea spune că Tcl sau Lisp îndeplinesc această cerinţă.

  • Să aibă un interpretor de mici dimensiuni -- În caz contrar, costurile înglobării acestuia în programul gazdă ar fi prea mari. Aşa s-a întâmplat în cazul aplicaţiei economice pe care am evocat-o în deschidere... dar fără această experienţă nu s-ar fi născut acest articol.

  • Să dispună de facilităţi puternice de descriere a datelor -- Cerinţa se justifică prin faptul că de cele mai multe ori configurările nu se rezumă la simple valori numerice sau şiruri de caractere, ci implică structuri complexe de date.

  • Să fie extensibile -- Aici se impune observaţia că diferite categorii de limbaje gazdă au pretenţii diferite de la limbajul de configurare, iar un limbaj de configurare care ar încerca să le satisfacă pe toate ar încălca cu siguranţă primele cerinţe. Soluţia nu poate fi decât posibilitatea de a specializa limbajul în funcţie de gazdă.

Un aspect important este faptul că limbajele de configurare nu sunt destinate dezvoltării unor programe ample, aşa că mecanismele specifice "programării în ansamblu" (programming at large) nu sunt esenţiale. Astfel, aspecte cum ar fi verificarea tipurilor sau încapsularea pot fi omise fără nici o remuşcare.

În fine, să mai notăm că limbajele de configurare mai sunt numite "limbaje de extensie", deoarece ele permit adăugarea unor facilităţi noi, definite de utilizatori. Prin intermediul acestor limbaje, aplicaţiile complexe tind tot mai mult să se structureze în două părţi:

  • nucleu (kernel) -- este scris de obicei într-un limbaj compilat şi cuprinde structurile de date şi funcţiile de bază ale programului, la care se adaugă interpretorul limbajului de configurare;

  • extensie (extension) -- este scrisă într-un limbaj flexibil, interpretat şi serveşte la stabilirea configuraţiei şi la definirea unor funcţiuni specifice aplicaţiei.

Implementarea unui limbaj de configurare se concretizează printr-un interpretor destinat executării secvenţelor de cod (chunks) scrise în acest limbaj şi o interfaţă de programare (API) care asigură comunicarea cu programul gazdă. De notat că această comunicare este bidirecţională, în sensul că, pe de o parte, programul gazdă trebuie să poată iniţia executarea secvenţelor în limbaj de extensie, iar pe de altă parte, aceste secvenţe să poată să utilizeze anumite funcţiuni pe care programul gazdă i le pune la dispoziţie.

Un limbaj catolic

Limbajul Lua (în portugheză înseamnă "luna") a fost dezvoltat în laboratorul TeCGraf al Universităţii Catolice Pontificale din Rio de Janeiro. Dezvoltarea nu a început, aşa cum s-ar putea crede, dintr-un proiect academic, ci din nişte necesităţi practice, legate de nişte contracte cu firme din industrie. Două programe de mari dimensiuni dezvoltate aici utilizau două limbaje de configurare diferite, concepute ad-hoc de proiectanţii aplicaţiilor. De aici a pornit ideea de a realiza un singur limbaj, care să poată fi extins astfel încât să fie utilizat în cadrul ambelor aplicaţii precum şi tuturor celor care vor veni.

Ceea ce a rezultat s-a numit Lua, un limbaj de configurare care respectă în totalitate cerinţele enunţate în secţiunea precedentă:

  • Sintaxa clară şi simplă -- Limbajul este structurat după modelul oferit de Pascal, cu diferenţa că structurile de control se încheie cu un end explicit (ca în Oberon), ceea ce simplifică parsarea şi interpretare şi sporeşte lizibilitatea.

  • Dimensiuni reduse -- Întreaga implementare constă din circa 6000 de linii sursă în ANSI C (în Windows: un DLL de 55KB). Bibliotecile adiţionale pentru calcule matematice, I/O, funcţii sistem şi manevrarea stringurilor mai adaugă doar 1000 de linii (în Windows: un DLL de 21KB).

  • Facilităţi de descriere a datelor -- Lua utilizează o singură structură de date, dar extrem de puternică: tablouri asociative dinamice. Prin intermediul acestora se simulează cele mai diverse şi mai complexe structuri. O construcţie specială, numită constructor, permite manevrarea extrem de simplă şi intuitivă a tablourilor.

  • Facilităţi de extensie -- Una dintre cele mai spectaculoase aspecte ale limbajului o reprezintă mecanismul de extensie bazat pe fallbacks.

Mai trebuie notat că Lua dispune şi de câteva extrem de valoroase facilităţi reflexive, prin care se pot implementa foarte elegant funcţionalităţi cu un înalt grad de polimorfism.

Fiind scrisă în ANSI C, implementarea limbajului este disponibilă pe platformele care dispun de compilatoare ce respectă standardul (gcc, Visual C++, Think C, CodeWarrior etc.). Adică, practic, pe toate.

La modul practic, bibliotecile Lua pot fi importate în limbajul gazdă (de regula C/C++, dar merge şi în altele - eu am testat în Oberon). Prin funcţiile puse la dispoziţie de aceste biblioteci se pot executa bucăţi de cod Lua (scrise în fişiere text sau furnizate ca stringuri), se pot seta sau citi variabile globale Lua, se pot înregistra funcţii care să poată fi lansate în Lua.

Deşi nu face parte din distribuţia standard, este disponibil şi un interpretor extern, pentru testarea secvenţelor de cod Lua şi pentru învăţarea limbajului. De fapt, acest interpretor poate fi scris în mai puţin de 10 linii de cod sursă C.

Copyright-ul este deţinut în continuare de TeCGraf, dar Lua poate fi utilizat în mod gratuit, chiar şi pentru scopuri comerciale. Sursele, documentaţia precum şi câteva distribuţii în format binar sunt disponibile prin mai multe situri Internet, pornind de la http://www.tecgraf.puc-rio.br/lua/ (sau o oglindă mai rapidă de la noi: http://csg.uwaterloo.ca/~lhf/lua/).

Elemente de bază

Sintaxa Lua, inspirată din Pascal şi C, este foarte liberă. Instrucţiunile pot fi eventual separate prin ";", dar sfârşitul de linie îndeplineşte acelaşi rol. Iată o funcţie care calculează factorialul:

      function factorial (n)
        if n == 0 then
          return 1
        else
          return n*factorial(n-i)
        end
      end -- sfirsitul functiei

Comentariile sunt introduse de "--" (ca în Ada) şi continuă până la sfârşitul liniei.

Ca majoritatea limbajelor interpretate, Lua se bazează pe tipizarea dinamică. Aceasta înseamnă că variabilele nu sunt tipizate şi nu trebuie declarate. În schimb, valorile sunt tipizate. Fiecare valoare poartă cu ea o informaţie de tip şi o etichetă (tag). O variabilă poate lua valori de orice tip şi, printr-o facilitate reflexivă, se poate testa tipul valorii conţinute de o variabilă la un moment dat. Funcţia standard type aplicată unei variabile determină tipul valorii.

Există doar şase tipuri de valori:

  • nil -- Este un tip care cuprinde doar valoarea nil, a cărei unică proprietate este că diferă de orice altă valoare. Semnificaţia ei uzuală este similară cu cea din Pascal, dar nimic nu ne împiedică să o utilizăm în alte scopuri.

  • number -- Reprezintă valori numerice reale (în dublă precizie). Pot fi scrise aproape oricum: ca numere întregi, ca numere cu zecimale (marca zecimală este punctul) sau în notaţie ştiinţifică (simbolul pentru exponent este "e" sau "E").

  • string -- Reprezintă şiruri de caractere. Caracterele sunt reprezentate pe 8 biţi şi pot fi scrise fie prin simbolurile cărora de corespund (dacă există aşa ceva), fie prin secvenţe escape similare celor din C ("\n" -- newline; "\t" -- tab etc.), fie prin codul lor exprimat zecimal pe trei poziţii ("\065" este "A"). Ca literale, şirurile se încadrează între ghilimele simple sau duble. O alternativă este scrierea între paranteze drepte duble, caz în care se pot întinde pe mai multe linii.

  • function -- În Lua funcţiile sunt considerate valori, deci pot fi stocate în variabile, pot fi furnizate ca argumente altor funcţii sau returnate de alte funcţii ca rezultat. Se pot apela funcţii scrise în Lua (cum este funcţia factorial de mai sus) sau funcţii "înregistrate" de programul gazdă (scrise de obicei în C). Funcţiile Lua şi funcţiile din C au acelaşi tip (function) dar au taguri diferite.

  • userdata -- Acest tip este furnizat pentru a permite stocarea în variabile Lua a unor structuri arbitrare de date din programul gazdă. Ele corespund construcţiei generice void* (cu alte cuvinte, sunt pointeri C) şi nu pot fi folosite (la modul direct) decât pentru atribuiri şi teste de egalitate.

  • table -- Acest tip implementează tablourile asociative dinamice, care reprezintă singura (dar foarte puternica) modalitate de structurare a dalelor în Lua. Tablourile asociative sunt pentru Lua ceea ce sunt listele pentru Lisp, motiv pentru care le voi dedica o secţiune specială.

Expresiile şi instrucţiunile sunt cele obişnuite, cu câteva excepţii. Una dintre ele o reprezintă atribuirea multiplă:

      a, b, c = 1, 2, 3

Această construcţie este echivalentă cu:

      a = 1
      b = 2
      c = 3

Un aspect interesant este că funcţiile pot returna mai multe valori, ceea ce poate conduce la expresii de genul următor:

      a, b = f(x)

Dacă funcţia f(x) întoarce două valori e ok şi expresia este echivalentă cu o atribuire multiplă. În caz contrar se face o "ajustare". Dacă funcţia întoarce mai multe valori decât variabilele din stânga, valorile suplimentare sunt ignorate (se pierd). Dacă întoarce mai puţine valori, ele sunt atribuite pe rând variabilelor din stângă, variabilele suplimentare căpătând valoarea nil.

Aţi remarcat cu siguranţă că operatorii sunt preluaţi din C ("=" pentru atribuire, "==" pentru egalitate etc.). Concatenarea şirurilor se face cu operatorul "..".

O ultimă remarcă referitoare la sintaxă: pentru a spori expresivitatea, Lua foloseşte în mai multe situaţii o sintaxă alternativă ("înlocuitor sintactic" - syntactic sugar). Definiţia funcţiei factorial este un exemplu. De fapt, sintaxa normală este:

      factorial = function (n)
        ...
        end

Tablouri asociative

Dacă sunteţi familiarizaţi cu tablourile asociative din alte limbaje (Perl, Tcl, Python, PHP) atunci singura diferenţă pe care trebuie să o notaţi este că în Lua tablourile nu pot fi alocate decât dinamic, ceea ce înseamnă că trebuie create explicit înainte de a fi utilizate. În schimb, fiind obiecte dinamice, pot fi manevrate ca nişte pointeri (de fapt chiar sunt pointeri). Asta înseamnă, printre altele, că puteţi referi un tablou dintr-un alt tablou, astfel încât puteţi înlănţui tablouri. Mai mult chiar, puteţi să referiţi un tablou chiar din interiorul său.

Dacă însă nu sunteţi familiarizaţi cu tablourile asociative, merita să o luam mai încetişor. În primul rând să dăm o definiţie (nu foarte riguroasă):

Un tablou asociativ este un tablou în care elementele pot fi indexate nu doar cu numere întregi, ci şi cu valori aparţinând altor de tipuri de date.

O primă constatare este că un tablou "obişnuit" (cel cu care sunteţi obişnuiţi din C sau Pascal) este tot un tablou asociativ. E drept, un caz particular, dar totuşi interesant. Iată cum se poate construi:

      numere = {} -- am creat 
      -- un tablou vid si l-am
      -- atribuit variabilei "numere"
      numere[1] = "unu"
      numere[2] = "doi"
      numere[3] = "trei"

Nimic neobişnuit, exceptând faptul că tabloul nu are limite prestabilite. Din fericire, Lua ne oferă şi o posibilitate mai simplă de a construi acest tablou:

      numere = {"unu", "doi", "trei"}

Această nouă formă ne relevă faptul că, în mod implicit, indicii pornesc de la 1 (spre deosebire de C). Pe de altă parte, observăm că tablourile de această formă seamănă grozav cu "mulţimea" (set) din Pascal şi cu listele din Logo. În fine, iată ce mai putem face:

      numere[99] = 99

Aşa ceva nu puteţi face în C sau Pascal! Tabloul numere are doar 4 elemente, deci indicii nu trebuie să fie consecutivi. Mai mult, nu există nici o ordine a elementelor. În plus, tablourile din Lua sunt eterogene (pot stoca orice fel de valori).

Elementele tabloului pot fi regăsite, desigur, prin indice:

      unu = numere[1]

Variabila numită unu a primit ca valoare stringul "unu". Există însă şi alte posibilităţi de a accede la elementele unui tablou: traversându-l. Iată o variantă:

      local i, a = next(numere, nil)
      while i do
        print(a)
        i, a = next(numere, i)
      end

Funcţia predefinită next primeşte ca argumente un tablou şi un indice şi returnează următorul indice şi valoarea asociată acestuia. Dacă indicele este nil atunci returnează prima pereche indice-valoare. Dacă indicele primit este ultimul din tablou, returnează nil. O varianta mai simplă:

      foreach (numere, print())

Funcţia foreach aplică funcţia primită ca al doilea parametru tuturor elementelor tabloului furnizat ca prim parametru.

Deci, chiar şi limitându-ne la varianta simplă, tablourile din Lua sunt extrem de versatile: sunt deschise, eterogene, nu pretind secvenţa indecşilor şi pot fi accesate atât prin indice cât şi prin traversare. Însă partea cea mai frumoasă abia acum începe: elementele pot fi indexate nu numai prin numere întregi, ci şi prin numere reale, şiruri de caractere, funcţii şi chiar alte tablouri.

Un prim exemplu pune în evidenţă utilizarea stringurilor ca indecşi:

      art = {nume = "Sarbu", prenume = "Mircea"}

Avem de-a face din nou cu o sintaxă alternativă. Lua interpretează această construcţie astfel:

      art = { ["nume"] = "Sarbu", ["prenume"] = "Mircea" }

Ceea ce este echivalent cu secvenţa:

      art = {}
      art["nume"] = "Sarbu"
      art["prenume"] = "Mircea"

Cu această ocazie am făcut cunoştinţă cu nouă formă de creare a unui tablou. Pentru ca să ne fie şi mai simplu, avem posibilitatea să combinăm cele două forme de creare, separând însă părţile prin ";":

      tab = {a="alfa", b=23; "plus", 7}

Desigur, accesul la valorile stocate poate face prin index:

      eu = art["prenume"] .. " " .. art["prenume"] 

Este deja foarte clar că utilizarea indexării prin stringuri ne permite să simulăm o altă structură tipică: articolul (record în Pascal, struct în C). Pentru a face şi mai expresivă această simulare, Lua ne pune la dispoziţie încă o sintaxă alternativă:

      eu = art.prenume .. " " .. art.nume

Prin această notaţie, articolele sunt simulate perfect.

Dacă încă nu v-am convins că tablourile asociative din Lua pot simula structuri de date oricât de complexe, urăriţi exemplul următor:

      function ins (root, val)
        if root then
          if val < root.info then
            root.left = ins(root.left, val)
          else
            root.right = ins(root.right, val)
          end
        else
          root = {info=val}
        end
        return root
      end

Este evident că avem de-a face cu un arbore binar. Iată şi o variantă de parcurgere, care aplică asupra tuturor nodurilor o funcţie 'f':

      function trav(root, f)
        if root then
          trav(root.left, f)
          f(root)
          trav(root.right, f)
        end
      end

Funcţia f ar putea fi, de exemplu, următoarea:

      function view(r)
        print(r.info)
      end

Deci, secvenţa următoare:

      b = ins(b, 2)
      b = ins(b, 7)
      b = ins(b, 3)
      b = ins(b, 5)
      trav(b, view)

va produce:

      2
      3
      5
      7

O menţiune finală se referă la posibilitatea de a defini constructori proprii, de forma:

      b = binar{2, 7, 3, 5}

De fapt este vorba de o sintaxa alternativă pentru:

      b = binar({...})

Funcţia binar o putem defini astfel:

      function binar(noduri)
        local b
        local i, a = next(noduri, nil)
        while i do
          b = ins(b, a)
          i, a = next(noduri, i)
        end
        return b
      end

Am obţinut astfel o variantă foarte comodă de creare a unui arbore binar.

Obiecte în Lua

Lua nu dispune de un suport adevărat pentru obiecte. În forma nativă nu există decât nişte forme sintactice alternative care ne dau senzaţia că lucram cu obiecte. Ideea este următoarea: din moment ce tablourile asociative pot stoca şi funcţii, am putea defini un obiect ca fiind un tablou cuprinzând funcţiile care-i sunt asociate şi eventual alte valori ("câmpuri"). Iată un exemplu simplu:

      obj = {info=2}

Acum, putem defini o "metoda" pentru obj astfel:

      function obj:add(val)
        self.info = self.info + val
      end

Invocarea metodei, urmată de o afişare de control:

      obj:add(13)
      print(obj.info)

Se va afişa, conform aşteptărilor, numărul 15.

De fapt, iată ce s-a întâmplat. Definiţia metodei este doar un substitut sintactic, pe care interpretorul îl transformă în:

      function dummy(self, val)
        self.info = self.info + val
      end
      obj.add = dummy

Deci stochează funcţia anonimă în tabloul obj sub numele pe care l-am precizat (add). Acesta este motivul pentru care am creat tabloul obj înainte de a defini metoda.

Apelul metodei este la rândul lui un substitut pentru forma:

      obj.add(obj, 13)

Din această formă se deduce cu claritate semnificaţia parametrului ascuns self.

Desigur, metodele aparţin obiectelor individuale, deci nu avem clase. Nu avem clase pentru că nu avem moştenire şi nu avem moştenire pentru că nu-i sigur că avem nevoie de aşa ceva într-un limbaj de configurare. Dar dacă, totuşi, vrem clase şi moştenire? Încă nu e totul pierdut: putem extinde limbajul.

Fallbacks

Cheia extensibilităţii lui Lua constă în existenţa unor funcţii speciale, numite fallbacks, care sunt destinate să trateze anumite situaţii neaşteptate care pot să apară în timpul execuţiei unei secvenţe: de pildă se încearcă să se adune un număr cu o funcţie sau să se extragă valoarea corespunzătoare unui index care nu există într-un tablou. Aceste funcţii în mod uzual nu fac mare lucru... De exemplu, la tentativa de a aduna un număr cu o funcţie provoacă o eroare, iar în cazul accesului la un index inexistent returnează nil.

Extensibilitatea se bazează pe posibilitatea de a înlocui aceste funcţii fallback cu funcţii utilizator. Fără să detaliez prea mult, ideea implementării moştenirii simple ar fi următoarea: o clasă se reprezintă ca o variabilă globală indicând un tablou asociativ cuprinzând metode. Printre aceste metode se poate număra şi una care produce obiecte şi le "leagă" de clasa respectivă (un constructor). Vom înlocui fallback-ul corespunzător accesului la un index inexistent cu o funcţie care verifică dacă tabloul apelat nu este cumva "legat" de un alt tablou (se poate presupune că este al clasei) şi în caz afirmativ se continuă căutarea în acel tablou. Altfel, se apelează vechiul fallback.

Dacă v-a plăcut...

Spaţiul alocat nu-mi permite să prezint aspecte mai avansate, cum ar fi comunicarea între programul gazdă şi Lua. O interesantă aplicaţie, numită CGILua, extinde funcţionalitatea serverelor Web astfel încât să poată utiliza scripturi Lua. Un alt subiect de interes l-ar putea reprezenta interfaţa dintre Lua şi Tk. Dar, ca de obicei, scopul acestui articol este doar să vă informeze ca "aşa ceva *într-adevăr* există" şi să vă trezească interesul pentru subiectul în discuţie.

Aşadar, dacă v-a plăcut... citiţi manualul! Ceea ce pot eu să vă asigur este că Lua funcţionează şi poate fi un instrument extrem de util în multe aplicaţii.


 

(Publicat în PC Report 97 - octombrie 2000)

 

Copyright © 2000 Agora Media

Creative Commons License
This work is licensed under a Creative Commons License.