Sunteți pe pagina 1din 33

Capitolul 1.

Introducere în limbaje de programare

Acest capitol introduce problematica limbajelor de programare. Pentru început


vom evidenţia necesitatea studierii limbajelor pentru a putea realiza
comunicarea om-calculator. Pornind de la comunicarea interumană prin
intermediul limbilor (vorbite, scrise, vizuale), vom defini conceptul de limbaj de
programare. Apoi vom insista asupra unor noţiuni introductive de limbaje de
programare. Astfel, vom realiza o clasificare a limbajelor de programare,
insistând asupra tipurilor de limbaje, asupra gradului lor de abstractizare
respectiv de apropiere de descrierea şi rezolvarea anumitor tipuri de probleme.
Vom continua prin a realiza o scurtă incursiune în istoria limbajelor de
programare, evidenţiind evoluţia lor în timp, concomitent cu evoluţia
paradigmelor de gândire în informatică. Partea finală a capitolului punctează
câteva aspecte caracteristice care trebuie studiate şi menţionate la referirea
unui limbaj de programare.

1.1. Definirea limbajelor de programare

Comunicarea interumană este indispensabilă în viaţa de zi cu zi. Pentru


realizarea acesteia, oamenii folosesc diverse modalităţi, limbajele reprezentând
poate cea mai importantă facilitate de comunicare. Una din primele aptitudini
importante pe care un copil o dobândeşte este să vorbească. El învaţă limba
maternă. De obicei, aceasta îi este suficientă pentru comunicarea de zi cu zi cu
alţi indivizi din propria societate. Dacă însă, un individ călătoreşte, el are nevoie
să cunoască şi alte limbi pentru a se putea descurca, pentru a putea comunica
cu alţi oameni din alte societăţi ale lumii. Cu toate că, în principiu, oamenii
atribuie înţelesuri similare pentru lucruri similare, ei vorbesc (exprimă) aceste
înţelesuri în limbi diferite. Diferenţa se manifestă atât la nivelul sunetelor şi
înlănţuirii acestora pentru a forma cuvintele cât şi la nivelul simbolurilor grafice
folosite. A învăţa să vorbim într-o altă limbă presupune a învăţa să rostim
cuvintele din limba respectivă, să le înlănţuim de o manieră potrivită conform
regulilor gramaticale a limbii învăţate şi să folosim simbolurile specifice acestei
limbi pentru a descrie vizual sintagmele de comunicare. Dacă nu mânuim
corespunzător aceste elemente, atunci când vom fi puşi în situaţia de a
comunica cu cineva care înţelege doar limba respectivă, vom eşua în
încercarea noastră.
10 Capitolul 1. Introducere în limbaje de programare

Am realizat această scurtă introducere pentru a putea realiza o comparaţie


între actele de comunicare inter-umană şi comunicarea dintre om şi calculator.
În această comunicare, putem vedea calculatorul ca pe un partener care e
dispus să ne rezolve problemele. Pentru aceasta, trebuie să-i specificăm modul
în care să rezolve aceste probleme. Putem să realizăm acest lucru utilizând
algoritmii. Dar algoritmii trebuie descrişi într-un limbaj inteligibil pentru
calculator. Putem vedea această comunicare ca un caz special al unei
solicitări. Dacă cerem ceva cuiva, atunci va trebui să folosim cuvinte, expresii
pe care acesta să le înţeleagă.
Conform Dicţionarului Explicativ al Limbii Române, prin limbă se înţelege un
sistem de comunicare alcătuit din sunete articulate, specifice omului, prin care
acesta îşi exprimă gândurile sau dorinţele. Astfel, un limbaj este un mijloc de
comunicare a ideilor prin sunete şi culoare, reprezentând un mijloc de
transmitere a informaţiei între indivizii unei categorii [Niţchi 2005].
Orice limbaj are la bază simboluri care formează limbajul respectiv. Astfel,
limba vorbită are la bază sunete, limbajele scrise au la bază literele, limbajele
vizuale au la bază simbolurile grafice. Semiotica este ramura ştiinţei care se
ocupă cu studiul simbolurilor.
Revenind la comunicarea dintre om şi calculator, trebuie să definim noţiunea de
limbaj de programare. Astfel, prin limbaj de programare înţelegem o notaţie
sistematică prin care este descris un proces de calcul [Pârv 1996]. Un proces
de calcul este constituit dintr-o mulţime de paşi pe care o maşină îi poate
executa pentru a rezolva o anumită problemă.
Astfel, un limbaj de programare este un intermediar între realitatea
reprezentărilor utilizatorului asupra problemei de rezolvat şi realitatea
calculatorului cu care lucrează [Şerbănaţi 1987]. La rezolvarea unei probleme
cu calculatorul, un programator trebuie să privească fiecare element al
limbajului din 2 puncte de vedere:
- unul logic, al problemei; astfel programatorul trebuie să ştie ce înţelege să
reprezinte din problemă cu ajutorul elementului de limbaj
- unul fizic, al implementării, care se referă la ceea ce realizează calculatorul
la execuţia elementului de limbaj considerat.
Programul este un compromis între cele 2 puncte de vedre. Astfel,
programatorul trebuie să-şi reprezinte şi să înţeleagă următoarele universuri:
- universul problemei
- universul limbajului de programare
- universul calculatorului
Aceste trei universuri sunt în general diferite. Pentru a realiza corespondenţa
între acestea, există definite diverse activităţi ale informaticii. Figura 1 surprinde
corespondenţa între universurile limbajelor de programare.
1.1. Definirea limbajelor de programare 11

Universul Universul
problemei calculatorului
Simulare

Elaborare
program Compilare

Universul limbajului
de programare
Figura 1. Universurile unui limbaj de programare [Şerbănaţi
1987].
Orice limbaj are 3 aspecte caracteristice:
- aspectul sintactic
- aspectul semantic
- aspectul pragmatic
Sintaxa unui limbaj conţine ansamblul regulilor prin care pornind de la
simbolurile de bază care alcătuiesc alfabetul limbajului, se construiesc structuri
compuse [Niţchi 2005].
Mulţimea regulilor sintactice care descriu ansamblul propoziţiilor sau a
formulelor corecte din cadrul limbajului formează gramatica.
Deci, sintaxa şi gramatica ne ajută să identificăm modul în care putem combina
simbolurile de bază ale limbajului pentru a produce elemente acceptate de
limbaj. Ele reprezintă imperative riguroase, care pot fi formalizate matematic.
Sintaxa se descrie teoretic cu ajutorul sistemelor formale. Următoarele
elemente se utilizează pentru descrierea sintaxei unui limbaj:
- diagramele sintactice
- arbori de analiză
- metalimbaje (BNF – Backus Naur Form, EBNF – Extended Backus Naur
Form, Asn.1)
Prin semantică se înţelege sensul construcţiilor sintactice. Ea reprezintă un set
de reguli ce determină semnificaţia propoziţiilor dintr-un limbaj. Este vorba de
reguli de evaluare a acestor propoziţii în termenii unor mulţimi de valori
cunoscute de limbajul respectiv. Astfel, semantica reprezintă înţelesul fiecărei
formule corecte admise de gramatică.
Pragmatica se referă la capacitatea de a utiliza construcţiile sintactice şi
semantice. Referitor la înţelegerea aspectului pragmatic al limbajelor, putem să
12 Capitolul 1. Introducere în limbaje de programare

se închipuim următorul exemplu: o persoană poate cunoaşte foarte bine


aspectele sintactice şi semantice ale unui limbaj, dar nu are capacitatea de a
utiliza corect aceste reguli. De asemenea, există persoane care vorbesc
(folosesc) o limbă fără a cunoaşte aspectele sintactice şi semantice ale
acesteia. Pragmatica nu se poate formaliza.
În capitolul 2 vom trece la prezentarea succintă a formalizării elementelor de
limbaje de programare.

1.2. Clasificarea limbajelor

Referitor la limbaje, putem să vorbim de o clasificare generală a acestora.


Astfel, limbajele pot fi naturale sau artificiale. Referindu-ne strict la limbajele de
programare, acestea pot fi clasificate din perspectiva paradigmelor de
programare sau a nivelului de abstractizare a limbajului. În această secţiune
vom descrie succint clasificarea generală a limbajelor şi vom insista asupra
clasificării limbajelor de programare.

1.2.1. Clasificarea generală a limbajelor


Pe plan general, există 2 tipuri de limbaje: naturale şi artificiale.
Limbajele naturale sunt cele istorice, precum limba română, engleză etc.
Limbajele artificiale au fost create de oameni în diverse scopuri. Astfel există:
- jargoane profesionale, folosite la comunicarea în cadrul diverselor grupuri
profesionale. Există astfel un limbaj matematic, medical, economic, bursier
etc.
- limbaje pentru comunicare între dispozitive. Aceste limbaje se mai numesc
şi protocoale. Ele ajută la stocarea şi transmiterea informaţiei între diverse
componente ale unor medii (de obicei artificiale). De exemplu, pentru
comunicarea în reţelele de calculatoare se foloseşte protocolul TCP/IP sau
alte protocoale de reţea (de nivel mai mult sau mai puţin abstract). Pentru
comunicarea la nivel software se pot folosi limbaje bazate pe descrieri
specifice de domeniu, descrise în XML.
- limbaje de pentru comunicarea dintre om şi dispozitive. Din această
categorie fac parte limbajele de programare.

1.2.2. Clasificarea limbajelor de programare din perspectiva


nivelului de abstractizare

Din punct de vedere al nivelului, limbajele se înscriu în 2 mari categorii:


- limbaje de nivel inferior
- limbaje de nivel superior
1.2. Clasificarea limbajelor 13

Cu cât un limbaj este mai apropiat de simbolurile efective, binare, cu care


lucrează calculatorul, limbajul este de nivel mai scăzut. Figura 2 descrie
ierarhizarea limbajelor de programare în funcţie de nivel.

Limbaj de nivel
superior

Limbaj de
Compilator
asamblare
Interpretor
asamblor

Microprocesor
Cod masina

Figura 2. Ierarhizarea limbajelor de programare în funcţie de


nivel.
Limbajul sau codul maşină reprezintă un limbaj binar cu care lucrează efectiv
procesorul. De obicei, fiecare procesor are propriul său cod maşină, definit fizic
de constructorul procesorului. Codul maşină aparţine primei generaţii de
limbaje de programare.
Limbajele de asamblare sau macroasamblare se situează deasupra codului
maşină şi sunt formate prin corespondenţă directă între succesiuni binare şi
termeni (eventual prescurtaţi) din vorbire. Limbajele de asamblare fac parte din
generaţia 2-a de limbaje de programare.
Programele scrise în cod maşină sau limbaje de asamblare pot fi considerate
optimale pentru calculator, deoarece acesta poate să execute în mod direct,
fără o altă conversie un asemenea cod. Dezavantajele principale ale acestora
rezidă în dificultatea acestor limbaje. Productivitatea activităţii de programare
este foarte scăzută. Limbajele din primele două generaţii sunt specifice pe
procesor, adică gradul lor de portabilitate este foarte scăzut. Prin portabilitate
se înţelege caracteristica unui program de a putea fi rulat pe diverse
calculatoare (sau tipuri de calculatoare) fără modificări, sau cu modificări
minimale.
Limbajele de nivel superior aparţin generaţiilor 3-a, 4-a şi 5-a de limbaje de
programare. Aceste limbaje se apropie tot mai mult de înţelegerea umană, de
reprezentări specifice în termenii problemei de rezolvat.
Limbajele de generaţia 3 conţin limbaje pentru prelucrarea datelor neorganizate
în baze de date. Ele sunt limbaje de uz general, respectiv de programare
automată. Aceste limbaje permit programatorului să exprime rezolvarea
problemei într-un limbaj mai apropiat de limbajul natural. Includem în această
categorie limbaje precum Fortran, C/C++, Pascal etc.
14 Capitolul 1. Introducere în limbaje de programare

Limbajele de generaţia 4 sunt destinate în special prelucrării datelor organizate


în baze de date. Ele sunt utilizate pentru realizarea de programe comerciale, a
căror obiectiv principal este manipularea unor cantităţi mari de date. Limbajele
din generaţia 4-a sunt de obicei uşor de învăţat şi aplicat, şi sunt destinate unor
categorii mai mari de utilizatori. Aceste limbaje pun accentul mai mult pe
funcţionalitatea oferită decât pe tehnicile de programare.
Limbajele din generaţia 5-a sunt limbajele inteligenţei artificiale. În această
categorie putem include limbajele logice pentru manipulare de simboluri şi
limbajele de gestiune a bazelor de cunoştinţe.
Limbajele de nivel superior prin caracteristicile lor, asigură o productivitate
corespunzătoare a muncii de programare. Astfel, ele se caracterizează prin:
- facilităţi legate de elaborarea programelor. Limbajul se apropie de multe ori
de limbajul natural sau de dialecte ale acestuia
- sunt standardizate: se asigură astfel independenţa de maşină şi deci
portabilitatea
- există o productivitate bună a elaborării programelor. Pentru multe limbaje
s-au dezvoltat metodologii de lucru în activitatea de dezvoltare a
programelor pentru a se asigura posibilitatea de lucru în echipă respectiv a
se asigura extensibilitatea programelor. Ingineria programării este ramura
ştiinţei calculatoarelor care se ocupă cu studiul metodelor de dezvoltare
organizată a programelor informatice.
Pentru execuţia programelor scrise într-un limbaj de nivel superior, acestea
trebuie convertite în cod de nivel inferior specific maşinii pe care se doreşte
rularea programului. Programele care asigură aceste conversii se numesc
programe de traducere. Programele de traducere sunt de 3 tipuri:
- compilatoare
- interpretoare
- traducere în două faze.
Compilatoarele traduc programul dintr-un limbaj sursă (de ex. C) într-un limbaj
obiect (de ex. cod maşină). Compilatoarele nu execută programul compilat.
Limbajul obiect nu trebuie să fie în mod obligatoriul limbajul maşină. Un tip
special de compilatoare sunt precompilatoarele care traduc o extensie a unui
limbaj într-un limbaj de bază. De multe ori activitatea de compilare este
realizată în mai multe faze. Vom insista asupra fazelor traducerii unui program
sursă în program obiect executabil în capitolul 2.
Interpretoarele traduc instrucţiune cu instrucţiune un cod sursă, şi pe măsură
ce realizează traducerea unei instrucţiuni o şi execută. Basic este un exemplu
de limbaj interpretat. Avantajul interpretoarelor faţă de compilatoare este că
dacă apare o eroare într-o instrucţiune ea se poate corecta pe loc. Limbajele
interpretate sunt însă, de obicei, limbaje simple, şi nu permit astfel realizarea
unor programe foarte complexe.
1.2. Clasificarea limbajelor 15

Traducerea în două faze presupune traducerea codului din cod sursă într-un
cod intermediar care apoi este rulat pe o maşină virtuală. Limbajele care
funcţionează pe acest principiu se numesc limbaje pseudo-compilative.
Exemple de limbaje pseudo-compilative sunt Java şi limbajele din familia .NET.

1.2.3. Paradigmele limbajelor de programare


Prin paradigmă se înţelege în general modelul sau arhetipul unui proces sau
sistem. Paradigmele de programare sunt colecţii individualizate de caracteristici
de evaluare şi criterii de abstractizare care determină şi diferenţiază clasele de
limbaje de programare [Pârv 1996]. Exemple de astfel de criterii sunt structura
programului, starea execuţiei, metodologia programării etc.
Clasificările limbajelor de programare după paradigme diferă de la autor la
autor. Vom prezenta în cele ce urmează câteva paradigme de programare
considerate mai importante în istoria limbajelor de programare [Niţchi 2005].
Trebuie să menţionăm că ideile cuprinse în limbajele de programare sunt în
continuă schimbare, datorită faptului că, permanent, apar limbaje noi, care de
multe ori nu pot fi clasificate conform criteriilor existente.

1.2.3.1. Paradigma programării procedurale


Clasificând limbajele după această paradigmă, ele se împart în limbaje
procedurale şi limbaje neprocedurale.
Limbajele procedurale se caracterizează prin faptul că utilizatorul descrie pas
cu pas algoritmul de rezolvare a problemei, printr-un şir de instrucţiuni care
formează o procedură, rutină, subrutină sau funcţie. Din acest punct de vedere,
programul este o mulţime ierarhică de blocuri şi proceduri. Exponentul clasic al
acestei paradigme este ALGOL60. Din această categorie amintim limbajele
Pascal, C, PL/I, Ada etc.
În limbajele neprocedurale, utilizatorul nu mai descrie algoritmul de rezolvare,
el doar indică sistemului ce anume doreşte. În acest caz, sistemul este
responsabil să găsească calea de rezolvare a problemei.

1.2.3.2. Paradigma programării structurate


Programarea structurată este în strânsă legătură cu programarea procedurală.
Mulţi autori consideră aceste două paradigme ca fiind de fapt una singură.
Fundamentul programării structurate este următorul enunţ: orice procedură
care are o singură intrare şi o singură ieşire poate fi reprezentată prin cele trei
structuri fundamentale din algoritmică: structura liniară, structura ramificată şi
structura repetitivă. Astfel, s-a demonstrat că instrucţiunile de salt necondiţionat
din programe nu mai sunt necesare, şi astfel se poate ordona activitatea de
scriere de cod. Alternativa la programarea structurată este programarea stil
16 Capitolul 1. Introducere în limbaje de programare

spagetti, care reprezintă un stil confuz de programare cu multe salturi


necondiţionate.
Recomandăm la nivel microscopic de programare utilizarea programării
structurate, adică folosirea în program doar a structurilor algoritmice
fundamentale. Un program structurat este totdeauna mult mai lizibil şi mai
flexibil decât unul nestructurat.
Exponentul principal al programării structurate este Pascal.

1.2.3.3. Paradigma programării modulare


Programarea modulară constă în descompunerea programului în module
independente, atomice. Principala idee este încapsularea. Astfel, datele interne
modulului sunt ascunse utilizatorului. Accesul la date se poate realiza doar prin
interfaţă. În acest context, un modul are două componente: interfaţa şi
implementarea.
Paradigma programării modulare extinde programarea procedurală şi
structurată prin introducerea unui nivel superior de abstractizare: modulul. Se
face astfel un pas important către programarea obiectuală.
Cele mai cunoscute limbaje modulare sunt Modula şi Ada.

1.2.3.4. Paradigma programării obiectuale


În această secţiune vom descrie doar succint caracteristicile paradigmei
obiectuale. Partea a doua a prezentei lucrări conţine o dezvoltare şi o
prezentare mai largă a conceptelor şi metodelor de programare obiectuală.
Un obiect defineşte o entitate care conţine informaţie şi are un comportament.
Alte obiecte pot face uz de comportamentul obiectului pentru a obţine
informaţia ataşată obiectului. În programarea obiectuală, orice entitate din
lumea reală poate fi considerată un obiect. Programul este considerat ca un
ansamblu de obiecte care interacţionează între ele.
În cadrul paradigmei obiectuale, obiectele reprezintă încapsularea datelor cu
codul. Deci obiectele au o structură şi o stare. Fiecare obiect defineşte
operaţiile care pot accesa şi manipula această stare. Obiectele sunt unităţi
atomice în programe. Datele interne ale obiectelor se mai numesc proprietăţi,
iar partea de cod formează metodele. Comunicarea între obiecte se face prin
mesaje. Astfel, un apel de metoda a unui obiect poate fi considerat ca o
transmitere de mesaj către obiectul apelat.
Obiectele de acelaşi tip pot fi grupate în clase. Din acest punct de vedere,
putem considera obiectele ca şi instanţieri ale claselor. Clasele pot fi relaţionate
unele cu altele, prin diverse tipuri de relaţii. Astfel, avem relaţiile de agregare
prin care o clasă se compune din alte clase, sau de generalizare/specializare,
1.2. Clasificarea limbajelor 17

prin care o clasă este caz particular (specializat) al unei superclase. Astfel, prin
compoziţie şi moştenire se asigură reutilizarea codului.
Principalul avantaj al programării obiectuale faţă de paradigmele anterioare
este realizarea polimorfismului. Prin polimorfism se înţelege proprietatea unui
nume de a fi utilizat cu înţelesuri diferite. Cea mai importantă formă de
polimorfism este polimorfismul de tip, prin care tipul referinţei se adaptează la
tipul obiectului.
Smalltalk este limbajul care a introdus paradigma obiectuală. Acest limbaj a
fost dezvoltat în limbajele Smalltalk80 şi Smalltalk86. C++ reprezintă extensia
obiectuală a limbajului structurat C. De asemenea Java implementează
caracteristicile paradigmei obiectuale.

1.2.3.5. Paradigma programării funcţionale


În cadrul acestei paradigme programul este descris ca un ansamblu de funcţii
apelate, în general, recursiv. Ele pot interveni în structurile de date ale
limbajului dar şi în cadrul codului, fiind permise funcţii de funcţii (se acceptă
operaţia de compunere de funcţii).
Printre caracteristicile acestor limbaje amintim faptul că ele nu au instrucţiuni de
atribuire şi nu generează efecte secundare la apelul unei funcţii. Prin efect
secundar la apelul unei funcţii înţelegem existenţa unui efect al rulării funcţiei
asupra apelantului ei.
Fundamentul matematic al limbajelor funcţionale îl constituie scrierea funcţiilor
ca şi expresii lambda.
Exemplul tipic de limbaj funcţional este LISP. El se bazează pe gestiunea
listelor. Un alt limbaj din această familie este ML, limbaj generic utilizat la
studiul teoretic al programării funcţionale.

1.2.3.6. Paradigma programării logice


În programarea logică un program este alcătuit din fapte şi reguli pe baza
cărora programul urmează să genereze concluzii. Limbajele din această
paradigmă se mai numesc limbaje declarative deoarece specificarea unui
program înseamnă specificarea unor declaraţii. Programul nu instruieşte
calculatorul cum să rezolve problema, ci spune calculatorului cum dorim să
arate soluţia problemei. În sensul programării logice, programul indică
procesorului metodele şi informaţiile necesare pentru extragerea unei soluţii de
forma dorită.
Baza matematică a acestor limbaje o constituie sistemele logice formale. De
obicei, acestea se bazează pe logica predicatelor de ordinul I, care poate fi
extinsă sau îmbogăţită cu alte axiome. Astfel, se pot obţine logici modale,
18 Capitolul 1. Introducere în limbaje de programare

temporale sau monotonice care pot să furnizeze mecanisme de deducţie


folosite în limbajele logice.
Prolog reprezintă exemplul clasic de limbaj din această categorie. El reprezintă
transcrierea într-un limbaj de programare a logicii predicatelor de ordinul I.
Limbajele logice stau la baza motoarelor de inferenţă din sistemele expert.

1.2.3.7. Paradigma programării concurente şi distribuite


Această paradigmă se bazează pe faptul că două sau mai multe programe,
respectiv părţi ale aceluiaşi program se execută în paralel pe acelaşi procesor
sau pe procesoare diferite. Astfel, această paradigmă nu mai respectă
principiul linearităţii în execuţia instrucţiunilor componente ale unui program.
Execuţia acţiunilor unui program poate fi independentă sau pot depinde de
execuţia instrucţiunilor altui program care se execută în paralel. Astfel, pentru
sincronizarea execuţiei programelor se folosesc tehnici de sincronizare şi
serializare, semafoare respectiv semnături de timp. Astfel, se asigură
comunicarea între procese, accesul concurent la resurse, partajarea resurselor.
Aspectul de distribuire se referă la utilizarea concomitentă resurselor din mai
multe locaţii, respectiv la împărţirea sarcinilor de executat şi realizarea acestora
de mai multe entităţi de procesare.
Limbajele concurente sunt de obicei extensii ale limbajelor din alte paradigme.
Astfel, există extensii concurente pentru C, Pascal, Fortran, Prolog. Exemple
de limbaje concurente sunt CSP, Linda, Occam, Parlog.

1.2.3.8. Alte paradigme de programare


Mulţi autori consideră şi alte paradigme de programare.
Astfel, pentru programarea la nivelul bazelor de date avem limbaje de
programare precum SQL sau dBase. Acestea sunt destinate asigurării unei
gestiuni corecte şi consistente a bazelor de date.
Unii autori consideră programarea vizuală ca o paradigmă de programare. În
cadrul acestei paradigme ei încadrează mediile de dezvoltare rapidă a
1
programelor . De obicei, însă aceste medii sunt destinate realizării activităţii de
programare pentru un anume limbaj, şi astfel, considerăm că ele nu reprezintă
o nouă paradigmă de limbaje de programare.
2
Limbajele bazate pe marcatori sunt folosite de obicei pentru structurarea şi
reprezentarea informaţiei şi mai puţin pentru realizarea activităţii de
programare. Ele se bazează pe SGML, un limbaj standardizat, independent de
platformă, utilizat la realizarea help-urilor şi a componentelor sale. În cadrul

1
Rapid Application Development (RAD) environments, în lb. Engleză
2
Markup Languages, în lb. Engleză
1.2. Clasificarea limbajelor 19

acestei categorii putem aminti XML, limbaj specializat pentru structurarea


informaţiei şi HTML limbajul World Wide Web-ului.
Programarea bazată pe agenţi reprezintă o nouă paradigmă de programare.
Agenţii reprezintă un nivel de abstracţie superior obiectelor, în sensul că agenţii
sunt entităţi software autonome capabile de comportament pro-activ. Aceasta
înseamnă, pe de-o parte, faptul că agenţii au o stare şi un comportament
propriu iar, pe de altă parte, faptul că acest comportament se poate executa din
dorinţa agentului, fără să fie nevoie invocarea comportamentului de o altă
entitate de program. Comportamentul agentului este ghidat mai mult de
obiectivele, credinţele şi intenţiile agentului decât de cereri explicite ale altor
agenţi. Teoria agenţilor reprezintă o nouă idee de realizare a programelor
software mai mult decât o bază care să conducă la realizarea de limbaje de
programare specifice. Totuşi, există limbaje specifice precum Telescript,
Agents, AgentTcl etc. De multe ori pentru realizarea de sisteme cu agenţi sau
care respectă principiile programării cu agenţi se utilizează limbaje de
programare obiectuale generale (precum Java) care sunt extinse cu facilităţi
destinate agenţilor. Se creează aşa-numitele platforme multi-agent în care se
pot crea (programa) agenţi care să rezolve anumite sarcini. Amintim în cadrul
platformelor multi-agent tool-urile FIPA-OS, JADE, IBM Aglets, etc. În cadrul
paradigmei programării cu agenţi se utilizează şi limbaje de reprezentare a
cunoştinţelor agenţilor şi de facilitare a comunicării inter-agent. În această
categorie putem înscrie KQML, KIF şi ACL.

1.3. Istoricul şi evoluţia limbajelor de programare

În această secţiune vom descrie principalele borne care au marcat istoria


limbajelor de programare, împreună cu elementele de noutate aduse de fiecare
limbaj important din punct de vedere a evoluţiei istoriei informaticii. Tabelul 1
prezintă o legarea principalelor limbaje de programare de momentul apariţiei.
Figura 3 conţine arborele limbajelor de programare, reflectând influenţele şi
genealogia limbajelor de programare. Acestea sunt preluate după [Pârv 1996].
Dezvoltarea limbajelor de programare este strâns legată de evoluţia ştiinţei
calculatoarelor. Astfel, pe măsură ce maşinile de calcul devin tot mai sofisticate
şi performante, evoluează şi limbajele şi paradigmele de programare, oferind
noi facilităţi utilizatorilor de maşini de calcul.
Limbaj An Autori Predecesor Scop
Fortran 1957-si J. Backus Calcule numerice
(IBM)
Algol60 1960-s Comitet Fortran Calcule numerice
Cobol 1960-s DOD Prelucrări de date
economice
20 Capitolul 1. Introducere în limbaje de programare

APL 1960-si K. Iverson Prelucrări de tablouri


(Harvard)
Lisp 1962 J. McCarthy Prelucrări de liste
(MIT)
Snobol 1966-si R. Griswold Prelucrări de şiruri de
(Bell) caractere
PL/1 1964-s IBM Fortran, Uz general
Algol60, Cobol
Simula67 1967 O.J. Dahl Algol60 Simulare
Algol68 1968-s Comitet Algol60 Uz general
Pascal 1971 N. Wirth (ETH Algol60 Uz general, scop
Zurich) educaţional
Prolog 1972 Univ. Marsilia Programare logică
C 1974 D. Ritchie Algol60, BCPL Programare de sistem
(Bell)
Concurrent 1975 P.B. Hansen Pascal Programare concurentă
Pascal (CalTech)
Euclid 1977 Comitet Pascal Programare de sistem
verificabilă
Modula 1977 N. Wirth Pascal Programare de sistem în
timp real
Ada 1979 J. Ichbiah (CII- Pascal, Uz general, aplicaţii hibride
Bull) Simula67
Smalltalk 1980 Xerox PARC Simula67 Programare orientată
obiect
C++ 1984 B. Stroustrup C Programare orientată
obiect
Oberon 1987 N. Wirth Pascal, Programare orientată
Modula obiect
Modula-3 1988 DEC, Olivetti Modula Limbaj puternic modular,
orientat obiect
Java 1995 Sun Limbaj orientat obiect,
traducere în două faze
3
Tabelul 1. Principalele limbaje de programare .
În 1944 maşina numită Mark I realizată de IBM a fost primul calculator capabil
să execute o secvenţă lungă de operaţii aritmetice şi logice. În 1946 ENIAC
(Electronic Numerical Integrator And Calculator) devine predecesorul primei
generaţii de calculatoare.

3
si înseamnă specificare şi implementare, iar s înseamnă numai specificare
1.3. Istoricul şi evoluţia limbajelor de programare 21

În 1959 apare a doua generaţie de calculatoare, bazată pe utilizarea


tranzistorilor. Calculatoarele din a 2-a generaţie sunt de dimensiuni mai reduse,
mai rapide, mai ieftine şi mai fiabile.

Fortran

Algol60 Cobol Lisp


1960

CPL
PL/1

BCPL
Algol68 Simula67

1970 Pascal

Prolog
C

Modula-2
Ada
1980 C cu clase Smalltalk

Objective
C
ANSI C

C++
Modula-3
1990

Ada9X

Figura 3. Arborele genealogic al limbajelor de programare.


În 1965 se introduc circuitele integrate, ceea ce reprezintă un semnificativ salt
tehnologic. În acest moment începem să vorbim de cea de-a 3-a generaţie de
calculatoare. Circuitele integrate au permis pentru prima oară obţinerea unei
viteze de calcul de peste un milion de operaţii pe secundă.
Din 1970 putem vorbi de cea de-a 4-a generaţie de calculatoare, prin
dezvoltarea microprocesorului. Acesta era un dispozitiv de calcul independent,
care, integrat în maşini de calcul a condus la realizarea unor computere mai
rapide, mai mici şi mai ieftine.
22 Capitolul 1. Introducere în limbaje de programare

Din acel moment până în zilele noastre am asistat la o adevărată explozie


tehnologică din punct de vedere hardware. Calculatoarele de azi sunt capabile
de performanţe greu de imaginat cu câteva decenii în urmă.
Bazele proiectării calculatoarelor au fost puse în 1940 de matematicianul John
von Neumann, prin descrierea arhitecturii von Neumann pentru maşini de
calcul. Această arhitectură a influenţat puternic modul de dezvoltare a artei
programării. Figura 4 descrie schematic structura unui calculator von Neumann.
Limbajele de programare care au derivat din această arhitectură sunt
cunoscute şi sub numele de limbaje imperative, datorită faptului că permit
programatorului să-şi specifice cerinţele printr-o secvenţă de instrucţiuni care
să indice exact ceea ce trebuie făcut pentru rezolvarea problemei.
intrari iesiri
Unitate aritmetica si logica
Unitate de control

Instructiuni Date

Memorie: programe si date

Figura 4. Arhitectura von Neumann


Punctele istorice pe care le-am marcat în paragrafele precedente au avut un
impact major asupra dezvoltării limbajelor de programare. Din momentul anilor
70, dezvoltarea limbajelor de programare a încercat să ţină pasul cu
dezvoltările hardware, dar evoluţia limbajelor de programare a luat o cale
distinctă. Vom descrie succint principalele limbaje de programare care au
marcat această istorie.
La mijlocul anilor 50, în cadrul firmei IBM, s-a studiat posibilitatea realizării unui
translator algebric. Astfel, echipa condusă de John Backus a realizat în 1954
specificaţia 0 a limbajului Fortran. Primul compilator de Fortran a fost realizat în
1957 odată cu specificaţia 2-a a acestui limbaj. Acest limbaj a devenit foarte
popular datorită faptului că firma IBM l-a oferit gratuit. Fortran reprezintă primul
limbaj de programare de nivel înalt. Are o sintaxă apropiată de scrierea
matematică. In Fortran linia de cod sursă are un format fix cu 4 câmpuri, tipul
variabilelor este specificat printr-o convenţie standard. Limbajul introduce
instrucţiunile IF pentru ramificare şi DO pentru realizarea de cicluri.
Limbajul Algol cu versiunea de referinţă Algol60 a avut o contribuţie
remarcabilă la dezvoltarea limbajelor de programare. Din Algol au derivat o
serie de limbaje de programare. Proprietăţile principale ale unui limbaj din
familia Algol sunt:
- este algoritmic
1.3. Istoricul şi evoluţia limbajelor de programare 23

- este imperativ (algoritmul este considerat o secvenţă de modificări ale


memoriei)
- are ca şi unităţi de bază blocul şi procedura
- conţine conceptele de tip şi verificare a tipului
- are o specificare prin reguli sintactice
- este compilativ
Algol nu a devenit foarte popular datorită faptului că nu a prevăzut intrări şi
ieşiri standard, nu avea un mecanism facil de transmitere a parametrilor prin
nume şi nu a fost susţinut de IBM (care a promovat puternic limbajul Fortran)
Tot la începutul anilor 60 a apărut şi cel de-al 3-lea limbaj de programare de
4
nivel înalt – Cobol . Limbajul se impune repede pe piaţă datorită sprijinului
puternic al guvernului SUA şi devine cel mai utilizat limbaj de programare în
deceniul 7. Cobol introduce descrierea datelor independent de maşină, punând
bazele sistemelor de gestiune a bazelor de date. Instrucţiunea if-then-else
apare pentru prima dată în formă completă în Cobol. Pentru creşterea lizibilităţii
5
programelor Cobol permite folosirea de „cuvinte în plus” .
Linia Algol este continuată cu limbaje precum Algol-W, Algol68, Euler şi mai
apoi Pascal. Tot pe linia Algol-ului apare şi limbajul Simula67 conceput pentru
aplicaţii în domeniul simulării. Simula67 este primul limbaj care introduce clasa,
considerată ca fiind un grup de declaraţii şi proceduri luate împreună şi tratate
ca o unitate distinctă de program.
Tot în această perioadă de început a limbajelor de programare apare limbajul
Lisp, realizat de John McCarthy la MIT. Acest limbaj introduce un nou tip de
programare, programarea funcţională. În Lisp, datele şi programele sunt
reprezentate uniform sub forma expresiilor simbolice. Lisp utilizează forma
prefixată a operatorilor, iar structura fundamentală de control este
recursivitatea. Lisp utilizează tehnica garbage collection în locul ştergerii
explicite a referinţelor. Lisp rămâne în continuare în actualitate fiind puternic
aplicat în inteligenţa artificială, mai ales în SUA.
Un alt limbaj important a fost PL/1. El împrumută concepte de la Fortran, Cobol
şi Algol68 şi introduce pentru prima dată gestiunea excepţiilor şi multi-taskingul.
PL/1 are un rol important în descrierea paralelismului.
La sfârşitul anilor 60 Niklaus Wirth a realizat limbajul Pascal, urmărind
realizarea unui limbaj de programare cu număr mic de concepte integrate.
Astfel Pascal furnizează un mecanism de structurare a datelor, care permite un
nivel superior de abstractizare. Hoare şi Wirth au oferit în 1971 definirea
axiomatică a limbajului. Limbajul Pascal permite verificarea programelor în faza
de compilare. Pe lângă aceste proprietăţi pozitive, totuşi Pascal are şi lipsuri,

4
COmmon Business Oriented Language, în lb. Engleză
5
Syntactic sugar, în lb. Engleză
24 Capitolul 1. Introducere în limbaje de programare

precum controlul insuficient al utilizării pointerilor, sau anomalii în mecanismul


de definire a tipurilor. În Pascal nu se pot folosi tablouri de dimensiuni variabile,
lucru posibil în Algol68.
Anii 70 s-au caracterizat prin concentrarea eforturilor de îmbunătăţire a stilului
de programare în limbajele imperative. Tendinţe majore au fost:
- dezvoltarea tipului abstract de date, apărut odată cu conceptul de clasă din
Simula. Tipul abstract de date a fost dezvoltat în limbajele Euclid, modula
şi Ada. El este tratat din două punte de vedere: relativ la proiectarea
limbajului de programare şi ca o proprietate a limbajului în sensul unei
facilităţi puse la dispoziţia programatorului.
- Perfecţionarea mecanismelor de gestiune a excepţiilor.
- Încorporarea de mecanisme pentru descrierea proceselor paralele. Astfel,
s-au dezvoltat concepte precum semaforul, monitorul sau sincronizarea
prin transmitere de mesaje. Modula şi Ada au facilităţi pentru tratarea
concurenţei.
Merită să amintim ca limbaje importante limbajul C, în care este scris sistemul
de operare Unix, şi limbajul Smalltalk care marchează începutul programării
obiectuale.
Ada este un limbaj important la sfârşitul anilor 70. Acest limbaj a fost promovat
6
de DOD în scopul de a unifica limbajele de programare în care sunt scrise
aplicaţiile militare. Ada este un limbaj de tip Pascal conţinând în plus facilităţi
pentru calcule numerice, intrări/ieşiri nestandard, specificarea dependenţei de
maşină, gestiunea excepţiilor, abstractizarea datelor şi concurenţă. Proprietăţile
limbajului acoperă toată gama conceptelor moderne de modularitate,
portabilitate, extensibilitate, abstractizare, facilităţi de dezvoltare a programelor
şi de întreţinere.
Anii 80 au fost marcaţi de dezvoltări în domeniul limbajelor pentru inteligenţă
artificială şi a paradigmei programării obiectuale. Astfel, apare generaţia 5-a de
limbaje de programare prin Prolog, iar limbajul Smalltalk evoluează şi apare
extensia C++ a limbajului de programare C. Tot în această perioadă se
standardizează o serie de limbaje de programare vechi (Fortran, Cobol şi
Pascal) şi apare standardul ANSI pentru C. Se dezvoltă limbaje de programare
pentru manipularea datelor în aplicaţii de gestiune precum dBase, FoxBase,
FoxPro, Reflex sau Paradox. Aceste limbaje au o sintaxă simplă şi sunt
destinate cu preponderenţă utilizatorilor neprofesionişti. Această perioadă este
marcată de începutul eforturilor pentru furnizarea unor medii de programare
puternice pentru diferite limbaje. Apar mediile de programare Turbo, editoare
de texte (WordStar, WordPerfect, MicrosoftWord), editoare pentru prelucrări de

6
United States Department of Defense
1.3. Istoricul şi evoluţia limbajelor de programare 25

tabele (Lotus, Borland Quattro, Excel), pachete de proiectare asistată CAD


(AutoCAD, ORCAD) şi utilitare de întreţinere.
Anii 90 sunt marcaţi de apariţia limbajelor de programare cu traducere în două
faze şi de limbajul Java. Acesta implementează într-o manieră mult mai clară şi
mai concisă paradigma obiectuală şi, prin rulare pe maşina virtuală Java
asigură portabilitatea programelor. În condiţiile proliferării paradigmei
obiectuale, Java s-a impus rapid, probabil şi datorită dezvoltării fără precedent
a Internetului. Totuşi, Java suferă la capitolul eficienţă la rulare, în sensul că
programele complexe Java necesită resurse de calcul şi de memorie sporite
pentru o rulare eficientă. Din acest punct de vedere, limbajul C++ rămâne
alegerea programatorilor atunci când criteriul de eficienţă este esenţial.
Succesul tehnologiilor Java, facilităţile deosebite de programare pe care acest
limbaj le oferă au impulsionat producătorii de limbaje de programare să-şi
reconsidere oferta. Astfel, Microsoft a lansat pachetul .NET care, spre
deosebire de pachetele MSVisual oferă o tratare în 2 faze a procesului de
compilare – execuţie a programelor.
Tehnologia obiectuală a devenit preponderentă în activitatea de programare, în
strânsă legătură cu dezvoltarea bazelor de date relaţionale şi a serverelor
puternice de date. Tehnologia obiectuală a fost capabilă să ofere un suport
consistent acestor cerinţe, astfel că a reuşit să se impună în dezvoltarea
aplicaţiilor comerciale. Preocuparea a fost standardizarea modului de
7
dezvoltare a software-ului. Astfel, eforturile OMG sunt remarcabile, şi au
8
culminat cu standardizarea specificaţiilor UML pentru proiectarea obiectuală.
UML este azi larg adoptat în firmele de software pentru descrierea procesului
de dezvoltare a produselor soft, în toate fazele acestuia, de la culegerea
cerinţelor, analiză, proiectare, implementare până la testare şi integrarea
produsului.
În prezent tendinţele dezvoltării limbajelor de programare conduc spre
standardizarea reprezentării cunoştinţelor, definirea şi înglobarea în limbaj a
unui nivel semantic, utilizarea facilităţilor inteligentei artificiale în realizarea
aplicaţiilor curente.

1.4. Întrebări propuse


1. Ce este un limbaj de programare? Descrieţi universul limbajelor de
programare.
2. Descrieţi cele 3 aspecte caracteristice ale unui limbaj de programare.
3. Descrieţi caracteristicile generaţiilor de limbaje de programare.

7
Object Management Group
8
Unified Modeling Language
26 Capitolul 1. Introducere în limbaje de programare

4. Descrieţi tipurile de programe de traducere.


5. Care sunt principiile programării structurate?
6. Descrieţi evoluţia paradigmelor de programare în ceea ce priveşte trecerea
de la procedură, la modul şi mai apoi la obiect.
7. Descrieţi asemănările şi deosebirile dintre paradigma programării
funcţionale şi programarea logică.
8. De ce este impropriu să considerăm XML un limbaj de programare?
9. Descrieţi caracteristicile esenţiale ale limbajelor de programare din anii 60.
10. Descrieţi caracteristicile esenţiale ale limbajelor de programare din anii 70.
11. Descrieţi principalele evoluţii ale limbajelor de programare în anii 80 şi 90.
Capitolul 2.
Fundamentele limbajelor de programare

În acest capitol vom introduce aspectele formale ale limbajelor de programare.


Astfel, vom defini limbajele abstracte, ca şi fundament pentru dezvoltarea
tehnicilor de parsare şi a compilatoarelor. Secţiunea 2-a a capitolului va
prezenta paşii şi operaţiile pe care le suportă un cod sursă pentru a fi
transformat în cod maşină, prin operaţia de compilare.

2.1. Limbaje abstracte

În acest capitol vom prezenta succint formalizarea exhaustivă a teoriei


limbajelor de programare. Materiale precum [Aho 1977], [Şerbănaţi 1987]
conţin o tratare detaliată a acestui subiect. Teoria limbajelor de programare
este necesară pentru specificarea formală corectă a limbajelor şi reprezintă un
fundament pentru teoria compilării.

2.1.1. Gramatici

Fie o mulţime A nevidă, finită, numită alfabet. Elementele acestei mulţimi se


numesc simboluri. Un simbol din A este reprezentat într-un limbaj printr-o
literă, cifră sau semn, uneori printr-un şir finit de litere, cifre sau semne.
Notăm prin A* mulţimea aranjamentelor cu repetiţie ale simbolurilor din A.
Astfel, un element din A* este un şir finit de simboluri din A . Simbolurile se
pot repeta în şir. Mulţimea A* conţine şi şirul vid; vom nota acest şir cu λ.
Numărul de simboluri dintr-un şir din A* se numeşte lungimea şirului. Se
observă că dacă avem două şiruri x şi y din A* atunci şirul z = xy ,
obţinut prin alăturarea simbolurilor din x cu simbolurile din y, va fi de
asemenea, element al mulţimii A*. Operaţia prin care se obţine şirul z din
şirurile x şi y se numeşte concatenare. Mulţimea A* înzestrată cu operaţia
de concatenare are o structură de monoid.
Prin definiţie, un limbaj formal peste alfabetul A este o submulţime L a lui
A*.
28 Capitolul 2. Fundamentele limbajelor de programare

Fiind dat un alfabet A şi mulţimea P ( A*) a părţilor mulţimii A * , pe această


din urmă mulţime putem defini următoarele operaţii de bază:
a. intersecţie: L1 ∩ L2
b. reuniune: L1 ∪ L2
c. complementare: L = {x ∈ A* | x ∉ L}
d. produs: L1 L2 = {x1 x2 | x1 ∈ L1 , x2 ∈ L2 } . Notăm cu L2 = LL,...
e. stea: L* = {λ} ∪ L ∪ L2 ∪ ... ∪ Ln ∪ ...
~ ~ ~
f. reflectare: L = {x | x ∈ L} unde x este imaginea reflectată a lui x
În legătură cu un limbaj formal, se pune problema apartenenţei unei construcţii
la limbaj, adică, în condiţiile furnizării unui cuvânt x ∈ A * , dacă se poate
decide (demonstra) una din următoarele 2 concluzii: x ∈ L sau x ∉ L .
Se spune că un limbaj L este decidabil, dacă răspunsul la întrebarea de mai
sus este pozitiv.
Conceptul de algoritm stă la baza rezolvării acestei probleme. Adică, trebuie să
decidem răspunsul la întrebarea de apartenenţă într-un timp finit folosind
operaţii precis definite. Problema apartenenţei unui cuvânt la limbaj, în cazul
limbajelor de programare trebuie să fie rezolvată de compilator, adică acesta
trebuie să decidă dacă codul sursă furnizat de programator satisface sau nu
regulile limbajului, adică poate fi compilat sau nu.
În cazul în care limbajul L este finit, atunci limbajul este decidabil. Dacă L
este infinit, trebuie să folosim alte metode pentru a răspunde la întrebarea de
decidabilitate.
Specificarea limbajului înseamnă fie enumerarea tuturor elementelor acestuia,
fie enunţarea unor reguli de construcţie a elementelor limbajului. Noţiunea de
gramatică stă la baza specificării unui limbaj prin generarea tuturor cuvintelor
sale.
Prin definiţie, un sistem formal este un cvadruplu ordonat S =< A, F ,ℵ, R >
alcătuit din:
- alfabetul sistemului A
- mulţimea decidabilă a formulelor corecte, F ⊆ A*
- mulţimea decidabilă a axiomelor, ℵ⊆ F
2.1. Limbaje abstracte 29

- R . O regulă de deducţie
mulţimea finită a regulilor de deducţie (inferenţă)
de aritate n + 1 este o relaţie din mulţimea F × F , care asociază o
n

formulă unică x cu un n-tuplu y =< y1 , y 2 ,..., y n > . Spunem că x se

deduce din y1 , y 2 ,..., y n şi scriem yRx .


Fie formulele corecte y1 , y 2 ,..., y n , numite premise. Fie

E0 = { y1 , y 2 ,..., y n } ∪ ℵ . Atunci, E0n reprezintă mulţimea tuturor n-tuplelor

cu formule din E0 . Atunci, aplicând succesiv regulile de deducţie din R


putem obţine mulţimile Ei = Ei −1 ∪ U{x | ∃y ∈ E , n
i −1 astfel incat yRx} .
n ≥1

Dacă mulţimea premiselor e vidă, adică E0 = ℵ atunci elementele lui Ei se


numesc teoreme. Dacă x este o teoremă, atunci ea s-a obţinut prin aplicarea
succesivă a unor reguli de deducţie asupra unor formule din mulţimile Ei .
Secvenţa acestor reguli de deducţie alcătuieşte demonstraţia teoremei x.
Prin definiţie, următorul caz particular de sistem formal:
a. alfabetul A al sistemului formal este finit şi este alcătuit din două mulţimi
disjuncte N şi ∑ , alfabetul de simboluri neterminale respective alfabetul
de simboluri terminale.
b. F = A * : toate şirurile finite pe alfabet sunt formule corecte.
c. ℵ conţine un singur element, şi anume un simbol neterminal S . Acest
simbol se numeşte simbol de început.
d. Regulile de deducţii au la bază producţii: O producţie este o pereche
(α , β ) de formule, notată cu α →β . Regula de producţie asociată
unei producţii este o regulă de rescriere, adică Pα Q → Pβ Q unde P
şi Q sunt 2 şiruri din F . Aceasta înseamnă că în orice formulă corectă
care conţine subşirul α se poate înlocui α cu β şi se obţine tot o
formulă corectă. Notăm cu P mulţimea finită de producţii.
În aceste condiţii, sistemul formal G =< N , ∑, P, S > se numeşte gramatică.
Procesul de inferenţă în cazul gramaticilor se numeşte derivare.
Spunem despre două şiruri δ şi ε din F că δ derivează imediat în ε în
cadrul unei gramatici date, dacă există două şiruri γ1 şi γ2 din F şi o
30 Capitolul 2. Fundamentele limbajelor de programare

producţie α →β astfel încât δ = γ 1αγ 2 şi ε = γ 1 βγ 2 . O asemenea


relaţie de derivare reprezintă o derivare într-un singur pas. Derivarea se poate
defini pe k paşi, în cazul ε este obţinut din δ prin k derivări.
O relaţie de derivare a lui ε din δ este nebanală, dacă obţinerea lui ε se
realizează într-un număr nenul de paşi. Relaţia de derivare se numeşte
generală dacă ε se obţine din δ într-un număr nenul de paşi sau dacă cele 2
formule coincid.
Prin definiţie, un limbaj generat de o gramatică G este mulţimea tuturor
propoziţiilor L(G ) = {x} cu proprietatea că S derivă general pe x.
Dacă limbajele generate de două gramatici coincid, se spune că gramaticile
sunt echivalente.
Conform definiţiei gramaticilor enunţată de mai sus, Naom Chomsky le-a
clasificat după forma producţiilor. Astfel, avem următoarele tipuri de gramatici:
1. Clasa gramaticilor de tip 0: reprezintă cea mai generală clasă de gramatici,
sunt cele care respectă definiţia generală furnizată mai sus.
2. Clasa gramaticilor de tip 1: producţiile sunt de forma: αAβ → αγβ . A
este un simbol neterminal, γ este şir oarecare, diferit de simbolul vid.
Aceste gramatici se mai numesc şi dependente de context.
3. Clasa gramaticilor de tip 2: producţiile sunt de forma: A → α , unde A
este un simbol neterminal, α este şir oarecare. Aceste gramatici se mai
numesc şi independente de context.
4. Clasa gramaticilor de tip 3 sau regulate: producţiile sunt de forma
A → aB sau A → a , unde A şi B sunt simboluri neterminale iar a
este simbol terminal.
Corespunzător claselor de gramatici, avem clase de limbaje. Astfel, se poate
defini o ierarhie Chomsky a limbajelor.
Limbajul independent de context (de tip 2) este modelul limbajelor de
programare.
În ceea ce priveşte utilitatea studiului gramaticilor pentru scrierea
compilatoarelor următoarea teoremă este importantă:
Orice limbaj dependent de context (şi în consecinţă independent de context şi
regulat) este decidabil. Deci, limbajele de programare sunt decidabile.
2.1. Limbaje abstracte 31

2.1.2. Specificarea limbajelor de programare

2.1.2.1. BNF
Pentru descrierea limbajelor de programare se folosesc meta-limbaje. Acestea
furnizează reguli prin care se pot specifica şirurile acceptate de un limbaj de
programare.
BNF (Backus Naur Form) reprezintă cel mai utilizat limbaj de specificare a
limbajelor de programare. În fond, BND descrie o gramatică independentă de
context. În BNF, simbolurile neterminare se scriu între paranteze unghiulare şi
se definesc recursiv, prin meta-formule. De asemenea, semnul ::= face parte
din limbajul de specificare. El se poate traduce prin: „se defineşte astfel”.
Simbolul | înseamnă alegere, în sensul că permite utilizarea uneia din cele 2
alternative alăturate simbolului. Astfel, în BNF, simbolurile <, >, ::=, | se
numesc meta-simboluri.
Pentru a exemplifica utilizarea BNF, vom descrie în acest limbaj de specificare
sintaxa de compunere a unei propoziţii simple într-un limbaj informal. Astfel,
avem următoarea definiţie BNF:

<sentence>::=<subject><verb><object>.
<subject>::=<article><noun>|<subject pronoun>
<verb>::=sees|hits
<object>::=<article><noun>|<object pronoun>
<article>::=a|the
<subject pronoun>::=he|she
<object pronoun>::=him|her

În consecinţă, se constată faptul că o propoziţie este compusă prin alăturarea


unui subiect, verb şi a unui obiect (atribut). Propoziţia se termină cu simbolul
terminal punct. Subiectul poate fi un articol alăturat unui substantiv sau
(simbolul |) un pronume de tip subiect. Pronumele de tip subiect poate fi unul
din simbolurile terminale he sau she etc.
Astfel, prin asemenea construcţii se pot descrie toate regulile de producţie din
limbaj. De fapt, prin BNF descriem întreaga gramatică pe baza căreia apoi,
putem genera limbajul corespunzător acesteia. Deci, pornind de la specificarea
BNF a unei gramatici, putem folosi această specificare pentru a genera limbajul
şi pentru a recunoaşte sintagmele acceptate de gramatica definită.
Procesul prin care, având dată la intrare o propoziţie, determinăm (decidem)
dacă propoziţia respectivă este acceptabilă, în contextul unei gramatici se
numeşte parsare.
32 Capitolul 2. Fundamentele limbajelor de programare

2.1.2.2. EBNF
De multe ori, specificarea BNF este greoaie din punct de vedere al lizibilităţii,
mai ales atunci când un simbol neterminal se poate repeta, de un număr finit
sau infinit de ori, într-o construcţie. De exemplu, putem considera definirea unui
string ca fiind un şir de una sau mai multe cifre:

<string>::=<digit>|<digit><string>

Pentru a uşura asemenea construcţii, EBNF introduce următoarele meta-


simboluri:
{} tot ce e inclus între acolade se poate repeta, sau poate lipsi
[] tot ce e inclus între parantezele drepte poate lipsi (este opţional)
Astfel, construcţia de mai sus poate fi scrisă:

<string>::=<digit>{<digit>}

De exemplu, pentru a defini un număr, care poate avea simbolul de semn,


putem folosi următoarele construcţii alternative:

<numer>::=<sign><unsigned>|<unsigned> în BNF

sau

<number>=[<sign>]<unsigned> în EBNF

EBNF introduce şi alte meta-simboluri ajutătoare. Astfel avem:


* înlocuieşte acoladele, are semnificaţia că simbolul precedent se poate
repeta de un număr de 0 sau mai multe ori
+ simbolul precedent se poate repeta de 1 sau mai multe ori
_ se subliniază meta-simbolurile, atunci când acestea fac parte din
alfabetul limbajului specificat

2.1.2.3. Diagrame de sintaxă


Reprezintă o descriere alternativă vizuală, a specificării unui limbaj. Au fost
introduse la specificarea limbajului Pascal. Diagramele de sintaxă reprezintă
grafuri orientate având ca noduri simbolurile din limbaj iar săgeţile indică
succesiunea acceptată a acestora.
Să considerăm o gramatică specificată în limbaj EBNF.
2.1. Limbaje abstracte 33

<sentence>::=<expression>=
<expression>::=<number>[<operator><expression>]
<number>::=[<sign>]<unsigned>
<operator>::=*|+|-|/
<sign>::=+|-
<unsigned>::=<string>[.<string>]|.<string>
<string>::=<digit>{<digit>}
<digit>::=0|1|2|3|4|5|6|7|8|9

Figura 5 prezintă câteva diagrame de sintaxă pentru elementele gramaticii de


mai sus.

<sentence> <expression> =

<digit> 0
1
2
3
4
5
6
7
8
9

<string> <digit>

Figura 5. Diagrame de sintaxă.

2.1.3. Automate de acceptare


Un automat de acceptare este folosit pentru a răspunde la întrebarea: un şir x
aparţine limbajului L sau nu?
Automatul este definit ca o maşină cu operaţii simple, care primeşte şirul de
analizat pe un suport oarecare, îl parcurge, şi răspunsul final este dat de starea
în care rămâne unitatea de comandă a automatului. Suportul pe care este
furnizat şirul este denumit generic bandă de intrare, iar variabila care parcurge
şirul de analizat în citire se numeşte cap de citire. Automatul poate folosi o
memorie auxiliară pentru păstrarea unor informaţii care să fie de folos la un
moment dat, în procesul decizional.
34 Capitolul 2. Fundamentele limbajelor de programare

Asemenea automate sunt definite prin grafuri. Nodurile grafului reprezintă stări
ale automatului, iar arcele reprezintă tranziţii între stări. Arcele sunt marcate cu
condiţii, cu semnificaţia că, dacă automatul se află într-o anumită stare, şi se
îndeplineşte condiţia de pe un arc care iese din starea respectivă, atunci
automatul va trece în starea de la celălalt capăt al arcului selectat.
Automatele pot fi:
- deterministe, dacă dintr-o stare se poate realiza cel mult o singură mişcare
- nedeterministe, dacă dintr-o stare există mai multe mişcări posibile.
Automatele nedeterministe corespund gramaticilor cu producţii cu
alternative.
Un şir x este acceptat de un automat U dacă, pornind de la configuraţia
iniţială, prin mişcările automatului se parcurge întregul şir de intrare şi
automatul ajunge într-o configuraţie finală.
Pentru exemplificare, vom considera automatul finit din figura 6.

b
a
a
q0 q1

Figura 6. Diagrama de tranziţii a unui automat finit cu 2 stări


Astfel, automatul din figura 6 are 2 stări: q0 şi q1. q0 reprezintă starea iniţială,
q1 reprezintă starea finală. Dacă, pe banda de intrare se întâlneşte şirul „a” şi
automatul este în starea q0, atunci automatul trece în starea q1. Dacă pe
banda de intrare se întâlneşte şirul „b” şi automatul este în starea q0, atunci
automatul rămâne în aceeaşi stare etc. Tabelul 2 prezintă matricea de definire
corespunzătoare acestui automat.
a b
q0 q1 q0
q1 q1 q0
Tabelul 2. Matricea automatului din figura 6
Pentru acest automat, şirul „aaba” este un şir acceptat deoarece:
(q0,aaba) a (q1,aba) a (q1,ba) a (q0,a) a (q1, λ )
Deci, pornindu-se din starea iniţială q0, se parcurge şirul de intrare şi în final, la
epuizarea acestuia, se ajunge în starea finală q1, cu şirul vid λ.
2.1. Limbaje abstracte 35

Exemplul din figura 6 reprezintă un automat finit determinist. În figura 7


prezentăm un exemplu de automat finit nedeterminist. Tabelul 3 reprezintă
matricea de tranziţii pentru acest automat. Caracteristic automatului
nedeterminist este faptul că dintr-un nod pot ieşi mai multe săgeţi etichetate cu
acelaşi simbol de intrare, precum şi săgeţi etichetate cu λ care reprezintă
tranziţii independente de intrare.

a
a b
0 1 3

b b

λ
2

Figura 7. Diagrama de tranziţie a unui automat finit


nedeterminist

a b λ
0 {0,1} - {2}
1 - {2,3} -
2 {2} {3} -
3 - - -
Tabelul 3. Matricea automatului finit din figura 7
Se poate stabili o relaţie între automatele finite deterministe şi cele
nedeterministe. Astfel, pentru orice automat finit nedeterminist există un
automat finit determinist care acceptă acelaşi limbaj. Vom prezenta succint în
cele ce urmează algoritmul de calcul al automatului finit determinist echivalent
din punct de vedere al limbajului acceptat cu un automat finit nedeterminist.
Considerăm un automat finit nedeterminist care conţine mulţimea de stări Q.
Fie ∑ mulţimea de simboluri de intrare acceptate de automat.

Vom construi automatul determinist cu mulţimea de stări Q1 pe aceeaşi


mulţime de simboluri de intrare ∑ astfel:
36 Capitolul 2. Fundamentele limbajelor de programare

Procedure AFN2AFD

Q1 ← {q0 }
while (mai există stări nemarcate în Q1
q ← [q1 , q2 ,..., qk ] o stare nemarcată din Q1
Se marchează q

for a ∈ ∑ do
M ← 0/
For i = 1, k

M ← M ∪ δ ( qi , a )
End for

q ' ← { p | p = δ ( M , λ )} - închiderea tranzitivă a lui M


if q ' ∉ Q1
Q1 ← Q1 ∪ {q '}
end if
La mulţimea de tranziţii a automatului determinist adaugă

tranziţia q → q' prin simbolul de intrare a


end for
end while
end procedure
Figura 8. Transformarea unui automat finit nedeterminist într-un
automat determinist echivalent din punct de vedere al limbajului
acceptat
În algoritmul din figura 8 s-au realizat următoarele notaţii:

- q0 reprezintă starea iniţială din automatul nedeterminist. q0 este închiderea


tranzitivă a stării iniţiale.
- δ (q, a ) reprezintă starea care urmează lui q în automatul nedeterminist prin
considerarea simbolului de intrare a.
- închiderea tranzitivă a unei mulţimi de stări este mulţimea tuturor stărilor la
care se poate ajunge considerând stări din mulţimea de intrare şi tranziţii doar
prin şirul vid.
Algoritmul prezentat este iterativ şi se bazează pe căutarea stărilor noului
automat prin includerea de stări din vechiul automat şi marcarea nodurilor deja
create. Vom exemplifica aplicarea acestui algoritm pe automatul nedeterminist
din figura 7.
2.1. Limbaje abstracte 37

Deci, starea iniţială q0 este nodul 0. Închiderea tranzitivă a acestui nod conţine
nodul 0 împreună cu toate nodurile în care se poate ajunge din acest nod
considerând doar tranziţii vide. Deci q0 este [0,2]. Acesta va reprezenta primul
nod din noul automat determinist. Marcăm acest nod, şi încercăm să
determinăm alte noduri ale automatului determinist. Pentru aceasta vom
considera, pe rând, fiecare simbol de intrare acceptat de automat. Fie pentru
început simbolul a. Construim mulţimea M ca fiind mulţimea tuturor nodurilor
destinaţie, pornind de la noduri din q0 şi considerând tranziţii acceptate de
automatul nedeterminist prin simbolul a. Obţinem M = {0,1,2} . Considerăm
închiderea tranzitivă a lui M , adică toate nodurile la care se poate ajunge din
noduri din M prin tranziţii vide succesive. După considerarea acestei operaţii,
mulţimea nou formată q' conţine aceleaşi noduri ca şi M . Extindem mulţimea

de noduri ale noului automat cu acest nou nod q ' = [0,1,2] . În automatul

determinist adăugăm tranziţia prin simbolul de intrare a din nodul q0 în nodul

q ' . Continuăm algoritmul prin considerarea celui de-al doilea simbol de intrare,
b. Considerând toate nodurile care urmează din noduri ale lui q0 prin b, vom
dezvolta mulţimea M = {3} . Închiderea tranzitivă a lui M este mulţimea
vidă. Deci, vom adăuga starea mulţime vidă în automatul determinist şi vom
lega această stare de starea iniţială prin tranziţia pe simbolul de intrare b.
Continuăm algoritmul prin marcarea unei noi stări, [0,1,2] , şi reluarea
aceloraşi paşi. Figura 9 conţine automatul finit determinist obţinut.
Transformarea unui automat finit nedeterminist într-un automat finit determinist
este importantă deoarece lucrul cu automatele nedeterministe este dificil,
datorită traiectoriilor paralele pe care le poate lua calculul în aceste automate.
Dar noi trebuie să privim aceste automate în contextul studiului gramaticilor,
care definesc limbajele de programare. Astfel, pornind de la o gramatică, se
poate genera un automat finit care să accepte gramatica respectivă. Dacă
automatul finit obţinut pentru o gramatică este nedeterminist, vom aplica
procedura din figura 8 pentru a construi automatul finit determinist echivalent.
Automatele deterministe ne indică modul în care trebuie să tratăm un şir de
intrare pentru a identifica dacă acesta este acceptat sau nu de automat. Astfel,
la construirea compilatoarelor, analizorul sintactic foloseşte logica automatului
pentru a spune dacă o construcţie de intrare e validă sau nu, şi în caz afirmativ,
consideră mai departe această construcţie.
38 Capitolul 2. Fundamentele limbajelor de programare

[2]
a

a
b
[0,1,2] [2,3]

b
b
a

[0,2] [3]
b
a,b

[0]

Figura 9. Automatul finit determinist echivalent automatului din


figura 7.
În practică, ori de câte ori avem de implementat un parser, adică un program
care interpretează la intrare şiruri de caractere cu anumite proprietăţi, prin
descrierea automatului corespunzător putem identifica traiectoriile posibile de
intrare şi implementa un analizor corect şi eficient. În ANSI C există biblioteca
de expresii regulate folosite pentru generare de şabloane de şiruri de caractere,
care urmează exact regulile semantice de descriere ale automatelor.

2.2. Compilatoare

În procesul de comunicare om-calculator intervine un program intermediar,


translatorul, care asigură traducerea programelor scrise de utilizator din cod
sursă într-un alt limbaj mai apropiat de calculator. Dacă limbajul ţintă este codul
maşină, translatorul se numeşte compilator. Astfel, execuţia unui program
sursă se realizează, în cadrul limbajelor compilative, în 2 faze:
- compilare, care traduce codul sursă în program obiect
- execuţie, care rulează codul obiect pe calculator, folosind datele iniţiale ale
programului şi produce rezultate
Compilatorul unui limbaj de asamblare se numeşte asamblor.
În practică, pe lângă compilatoarele obişnuite, există şi alte tipuri de
compilatoare.
Astfel, preprocesoarele sunt translatoare care traduc dintr-un limbaj de nivel
înalt în alt limbaj de nivel înalt. Preprocesorul limbajului C++ reprezintă un bun
exemplu.
2.2. Compilatoare 39

Cross-compilatoarele sunt compilatoare scrise pentru un calculator gazdă, în


vederea generării de cod pentru alt calculator. Cross-compilatoarele sunt
folosite la scrierea de cod pentru diverse dispozitive inteligente, care conţin
procesoare.
În cazul limbajelor interpretative, compilatorul este de tip special, adică
incremental. Astfel, programul sursă este spart de către compilator în porţiuni
mici numite incremente, care au o oarecare independenţă sintactică şi
semantică. Incrementele sunt traduse de compilator. Pe măsură ce
compilatorul traduce un increment, calculatorul execută incrementul tradus.
În mod tradiţional, un compilator realizează un şir de transformări asupra
codului sursă în reprezentări din ce în ce mai apropiate de codul maşină.
Figura 10 prezintă fazele unui compilator.
Program Sir de atomi Arbore Cod Cod intermediar Program
sursa lexicali sintactic intermediar optimizat obiect

Analiza Analiza Analiza Optimizare Generare de


lexicala sintactica semantica de cod cod

Tratarea Gestiunea
erorilor tabelelor

Figura 10. Fazele unui compilator [Şerbănaţi 1987].


Analiza lexicală grupează caracterele din program în subşiruri numite atomi
lexicali care reprezintă cuvintele cheie, operatori, constante, identificatori şi
delimitatori.
Şirul de atomi lexicali este preluat de analiza sintactică. Aceasta depistează
structuri sintactice cum ar fi expresii, liste, instrucţiuni, proceduri. Aceste
structuri sunt plasate într-un arbore sintactic conform relaţiilor existente între
aceste structuri.
Analiza semantică foloseşte structura programului pentru extragerea
informaţiilor privind obiectele purtătoare de date (variabile, proceduri, funcţii),
verificarea consistenţei utilizării lor. Pe măsura parcurgerii arborelui sintactic,
analiza semantică construieşte o reprezentare a codului sursă în cod
intermediar. Acesta este de obicei un şir de instrucţiuni simple cu format fix.
Ordinea operaţiilor din codul intermediar respectă ordinea de execuţie a
acestora pe calculator.
Codul intermediar este prelucrat în faza de optimizare pentru eliminarea
redundanţelor de calcule, a calculelor şi variabilelor inutile, pentru o execuţie
mai eficientă.
40 Capitolul 2. Fundamentele limbajelor de programare

Generarea de cod alocă celule de memorie pentru memorarea datelor la


execuţie. Se alocă registre şi se produce cod obiect echivalent cu programul în
limbaj intermediar.
Gestiunea tabelelor este de fapt, o colecţie de proceduri care creează şi
actualizează datele cu care lucrează celelalte faze. În această tabelă, pe lângă
informaţii proprii compilatorului se găsesc şi tabele ale identificatorilor,
constantelor, cuvintelor cheie. Uneori avem o tabelă unică, numită tabela
simbolurilor.
Tratarea erorilor este o colecţie de proceduri care sunt activate ori de câte ori
se depistează o greşeală în program. De obicei, utilizatorul primeşte un mesaj
de diagnostic. Dacă greşeala este identificată în faza de analiză sintactică,
compilatorul poate să-şi urmeze analiza pentru a detecta şi alte erori.
Structura prezentată este mai mult conceptuală. Compilatoarele concrete de
multe ori prezintă abateri faţă de această structură. Unele componente pot lipsi,
sau funcţionalitatea lor poate fi preluată de alte componente, sau ordinea
activării componentelor poate fi diferită.
Realizarea compilatoarelor presupune un volum mare de muncă. Există unelte
software specializate care asigură facilităţi de dezvoltare a compilatoarelor.
Astfel, avem uneltele LEX şi YACC sub Unix care permit descrierea sintactică
şi semantică a unui compilator, conform regulilor limbajului, utilizând
automatele de acceptare a construcţiilor de intrare. Programatorul trebuie să
descrie regulile limbajului şi sintaxa propoziţiilor acceptate în format de expresii
regulate, iar mai apoi construcţia ţintă asociată fiecărei sintagme de limbaj
sursă. Utilitarele generează un cod C care apoi compilat, reprezintă de fapt
compilatorul pentru limbajul sursă considerat.
Datele de intrare pentru asemenea unelte sunt:
- specificaţia limbajului sursă, în ceea ce priveşte descrierea lexicului şi a
sintaxei
- specificaţia limbajului ţintă şi a regulilor semantice de traducere
- specificaţia maşinii ţintă
Dacă în trecut, timpul de elaborare a unui compilator era destul de mare, azi,
utilizând asemenea tool-uri moderne putem realiza rapid compilatoare pentru
diverse limbaje.

2.3. Întrebări propuse

1. Descrieţi clasificarea gramaticilor după Naom Chomsky.


2. De ce limbajul independent de context reprezintă modelul unui limbaj de
programare?
2.3. Întrebări propuse 41

3. Cum se specifică un automat? Care sunt tipurile de automate? Explicaţi


importanţa studierii automatelor cu privire la studiul limbajelor de
programare.
4. Explicaţi structura (fazele) unui compilator. Ce se întâmplă în fiecare fază?
5. Se dă următoare specificare EBNF pentru un calculator de buzunar:
(a) <expression>::=<number>{<operator><expression>}
(b) <sentence>::=<number>{<operator><sentence>}=
(c) <number>::={<sin>}<unsigned>
Sunt aceste reguli de specificare corecte?
6. Fie următoarele producţii:
<unsigned>::=<string>[.<string>]
<string>::=<digit>{<digit>}
<digit>::=0|1|2|3|4|5|6|7|8|9
Se decidă care din următoarele numere sunt acceptate sau nu de gramatica
specificată mai sus:
(a) 1
(b) 1.
(c) 1.1
(d) .1
(e) 12.34
7. Să se scrie automatul finit determinist care acceptă următorul limbaj: {a, b,
ab, abab, …}
8. Fie gramatica regulată G=<{B, S}, {a, b}, P, S> unde
P = {S → λ , S → aB, B → aB, B → bB, B → a, B → b} .
Descrieţi limbajul aferent acestei gramatici în BNF. Trasaţi automatul finit
care acceptă această limbajul asociat gramaticii. Aplicaţi procedura de
conversie a automatului nedeterminist în automat finit determinist şi listaţi
automatul determinist echivalent.

S-ar putea să vă placă și