Let's Do It Romania - 24 Septembrie 2011



   

Component Pascal

   

O propunere pentru învăţământul informatic românesc


   

Un dialect al limbajului Oberon ar putea să înlocuiască limbajul Pascal în primii ani de studiu al limbajelor de programare. Acest articol încearcă să vă convingă de avantajele pedagogice ale limbajului Component Pascal şi să vă atragă atenţia asupra unor caracteristici tehnice mai puţin uzuale.

Mircea Sârbu


Iată, deci, care este problema: în liceele de informatică şi un multe facultăţi, primul limbaj folosit este Pascal. Întotdeauna am susţinut Pascal-ul în competiţia - pierdută, de altfel - cu limbajul C şi, mai apoi, C++. Sintaxa clară, structura ireproşabilă, tipizarea ceva mai riguroasă, nivelul suficient de înalt, expresivitatea, plus încă multe altele, au făcut din Pascal limbajul predilect pentru învăţarea programării, mai ales din perspectivă algoritmică.

Cu toate acestea, timpul a trecut şi Pascal-ul a rămas în urmă. Faptul că utilizarea limbajului în medii productive s-a redus la câteva dialecte - mă refer mai cu seamă la Delphi sub Windows şi Object Pascal pe Mac - nu este un impediment: odată ce tehnicile fundamentale au fost corect însuşite, adaptarea unui tânăr la noi limbaje este uşoară şi chiar firească. În paranteză fie spus, cred - şi nu doar eu - că este preferabil ca limbajele învăţate în şcoală să nu fie cele mai în vogă în mediile productive.

Care sunt, atunci, neajunsurile Pascal-ului? Iată câteva:

  • Înainte de toate, în tot învăţământul românesc, compilatoarele de la Borland (actualmente Imprise) sunt folosite ilegal. Nu este moral ca şcoala să-l înveţe pe elev cu eludarea drepturilor de autor, faptă sinonimă că furtul.

  • Deşi există compilatoare pentru Windows, complexitatea programării Windows obligă profesorii şi elevii să se mărginească la mediul DOS. Pentru algoritmică, este corect. Dar faptul rămâne: se lucrează într-un mediu depăşit.

  • Paradigmele moderne de programare, cum ar fi orientarea pe obiecte, programarea pe bază de evenimente sau programarea vizuală depăşesc posibilităţile limbajului. Extensiile obiectuale din Borland Pascal nu sunt calea cea mai didactică spre lumea obiectuală.

  • Lucrul în DOS mai are un dezavantaj: utilizarea instrumentelor software uzuale, de tipul programelor de birotică (parte a unei alfabetizări informatice absolut inevitabile) se învaţă în cadrul unor alte cursuri.

Se poate continua, dar nu acesta este scopul acestui articol.

Ok. Dacă nu Pascal, atunci ce? Este evident că C++ nu este o alternativă viabilă, mai cu seamă datorită complexităţii (eu personal consider că nu este nici suficient de "expresiv"). Am auzit multe voci care cereau Java. Unii au propus Python. Alţi s-au orientat spre Logo (mai ales pentru copii). Există universităţi unde se începe cu Smalltalk, altele unde se învaţă Scheme...

Ceea ce se uită este faptul că elevii şi studenţii sunt instruiţi de nişte profesori. Or, în mediul liceal cel puţin, marea majoritate a acestora ştiu Pascal şi doar accidental alte limbaje. Nu toţi vor fi entuziasmaţi de perspectiva de a învăţa peste noapte un limbaj nou. Pe de altă parte, există o mare cantitate de "material didactic" bazat pe Pascal, există o adevărată tradiţie, poate chiar şi o doză de inerţie...

Oberon

Profesorul Wirth, cel care a creat în 1970 limbajul Pascal, s-a pensionat abia anul trecut (o ciudată formă de protest faţă de direcţia - greşită, în opinia sa - pe care a urmat-o industria de software... dar asta-i altă poveste). În toţi aceşti ani, Wirth a desfăşurat o intensă activitate - didactică, dar mai cu seamă de cercetare - care i-a adus o largă şi indiscutabilă faimă mondială. La Xerox PARC (Palo Alto Research Center) Wirth a lucrat la dezvoltarea unor noi staţii de lucru, astfel încât cercetările au mers spre dezvoltarea unor limbaje mai puternice, primul pas fiind Modula-2 (1979).

După ce modelul constituit de Smalltalk a deschis calea obiectuală pentru probleme de uz general şi a inaugurat era interfeţelor grafice, eforturile au vizat realizarea unor sisteme de operare care să susţină noile paradigme, dar care să fie mai sigure şi mai performante. Pentru aceasta era nevoie de un limbaj de nivel înalt, cu tipizare strictă, care să poată fi compilat (Smalltalk foloseşte o maşină virtuală care interpretează un bytecode) şi să susţină extensibilitatea sistemului de operare. Cedar a fost o tentativă de limbaj şi sistem de operare, care însă s-a dovedit prea complex pentru gusturile profesorului With (dictonul său preferat fiind preluat de la Einstein: "Make it as simple as possible, but not simpler").

Reîntors în Elveţia natală, Wirth a început la ETH Zurich un proiect asemănător: un sistem de operare modern pentru staţiile Ceres (1985). După ce a rafinat la maximum ideile din Cedar, Wirth a preluat simplitatea structurii din Pascal şi modularitatea din Modula-2, a adăugat orientare pe obiecte şi a rezultat un limbaj mai simplu decât Pascal şi mai puternic decât Modula-2. Noul limbaj, numit Oberon, a permis realizarea unui sistem de operare (numit tot Oberon) care rula foarte bine în 2 MB RAM şi avea nevoie doar de 10 MB pe disc.

În 1992 a urmat o revizie a limbajului, cu care ocazie s-au adăugat câteva elemente importante, şi limbajul a devenit Oberon-2. Au urmat o serie de implementări realizate la ETH, pentru diverse platforme, fie ca sistem de operare nativ, fie (mai ales) sub forma unui framework funcţionând peste sistemul de operare gazdă. Tot în 1992 ia naştere firma Oberon Microsytems (un spin-off al ETH, cu Wirth în consiliul de administraţie), care încearcă să aducă tehnologia din Oberon spre mediile comerciale. Mediul Oberon este reconstruit peste Windows şi MacOS sub numele Oberon/F şi, mai apoi, BlackBox Component Framework. Limbajului i se adaugă (în 1997) câteva facilităţi suplimentare, noul dialect primind numele Component Pascal.

Între timp, la Institutul Federal de Tehnologie (ETH) din Zurich, la Universitatea din Linz, şi în alte câteva mari institute din lume se lucrează în continuare cu Oberon-2 şi mediul original. Acesta, spre deosebire de BlackBox, furnizează o interfaţă grafică proprie, independentă de sistemul de operare gazdă: sistem propriu de ferestre, funcţionalitate extinsă a mouse-lui etc.

Avantajele

Înainte de toate, trebuie să precizez că BlackBox este disponibil gratuit pentru utilizare în domeniul educaţiei (versiunea educaţională nu diferă de cea comercială decât prin licenţă). Există şi o versiune educaţională de reţea, special pentru şcoli, în care mediul este încărcat de pe un server (această versiune se furnizează la cerere). Deşi aceste condiţii rezolvă problemele legale şi economice legate de utilizarea softului, Ministerul Educaţiei ar putea negocia şi unele aspecte contractuale cu firma elveţiană (o versiune specială, instruire etc.).

Voi trece în continuare în revistă câteva dintre avantajele tehnice ale utilizării limbajului Component Pascal şi a mediului BlackBox (pe care le voi denumi adesea Oberon) în învăţământ.

  • Necesită resurse hardware minime. Practic, orice calculator care poate să ruleze Windows (chiar şi 3.1) poate rula eficient Oberon. Deoarece nu se păstrează informaţii de stare, fiecare post de lucru poate fi uşor administrat pentru a fi folosit de zeci de studenţi (situaţie tipică în şcoli).

  • Mediul de lucru este complet şi integrat. Cuprinde un subsistem de text (echivalent cu un procesor de text modern), un subsistem de forme (care permite programarea aşa-zis "vizuală"), un subsistem de dezvoltare (cu unul dintre cele mai performante compilatoare pe 32 de biţi, cu un sistem de depanare de tip hipertext, cu editor de legături, cu încărcător dinamic cu verificarea versiunilor etc.) şi cu o largă bibliotecă de module. Întregul sistem este deschis şi extensibil. Întreaga distribuţie încape pe 4 dischete (nu CD-ROM-uri, dischete!).

  • Limbajul este mai simplu decât Pascal. Un cunoscător de Pascal poate învăţa Oberon în câteva ore. Orice program Pascal poate fi transpus foarte simplu în Oberon. Întreaga experienţă pe care profesorii au dobândit-o în Pascal rămâne valabilă în Component Pascal.

  • Oberon este un limbaj hibrid, procedural şi obiectual. Spre deosebire de Pascal, aspectele obiectuale sunt parte integrantă a limbajului, oferind şi o cale extrem de directă şi simplă către înţelegere a orientării spre obiecte.

  • Limbajul este eficient şi expresiv atât pentru probleme de algoritmice, cât şi pentru programare productivă (dispune de un modul de interfaţă cu baze de date, de o interfaţă de comunicaţii TCP/IP etc.), pentru programare de sistem (întregul mediu BlackBox a fost scris în Component Pascal), dar poate fi folosit şi ca un limbaj de scripting.

  • Limbajul este conceput pentru a permite dezvoltarea sistematică a proiectelor de dimensiuni mari iar modularitatea şi accentul pus pe interfeţe îl recomandă pentru munca în echipă. Separarea logici procedurale de interfaţa cu utilizatorul facilitează crearea rapidă de prototipuri. Orientarea pe componente şi utilizarea documentelor compuse îl plasează în zona cea mai modernă a tehnologiei.

Desigur, posibilitatea de a traduce (la rece, fără recomplilare) toate elementele de interfaţă cu utilizatorul în orice limbă, inclusiv română, nu reprezintă un argument serios (oricum elevii trebuie să se familiarizeze cu engleza). Aşa cum, din păcate, nici compatibilitatea la nivel de document cu versiunea pentru Macintosh nu este importantă la noi. Pe de altă parte, diferenţele faţă de standardul Oberon-2 fiind minime, studenţii pot avea imediat acces la versiunile pentru diverse "arome" de Unix (inclusiv Linux) ale distribuţiilor Oberon de la ETH şi Universitatea din Linz. Mai mult, există compilatoare independente, de cea mai bună calitate, cel furnizat împreună cu mediul Pow! fiind un excelent exemplu (mai ales că este gratuit).

Diferenţe

Deşi pare greu de crezut, Oberon este mai simplu decât Pascal (definiţia formală a limbajului este redusă aproape la jumătate). Wirth a eliminat toate caracteristicile care nu i s-au părut esenţiale, astfel încât fiecare aspect al limbajului să exprime concepte simple şi clare.

Au dispărut, astfel, tipurile enumerate şi interval (subrange), tablourile nu mai pot fi indexate decât prin întregi (pornind de la zero), au dispărut articolele cu variante, la fel şi mulţimile generale (care acum pot conţine doar întregi de la 0 la 31) precum şi anacronica instrucţiune goto...

Există o serie de modificări minore de sintaxă, dintre care voi aminti doar faptul că instrucţiunile de control al fluxului de execuţie (IF, FOR, REPEAT, WHILE etc.) se termină cu un END, eliminându-se astfel cerinţa ca secvenţele de instrucţiuni să fie încadrate de "begin" şi "end". Şi dacă tot veni vorba: literele mari sau mici contează.

A apărut o nou structură de control: bucla (LOOP) cu ieşire explicită (EXIT). Instrucţiunea IF a fost îmbogăţită cu o variantă de înlănţuire a testelor (cu ELSIF), în timp ce instrucţiunea WITH a căpătat o nouă întrebuinţare - nu mai marchează un domeniu de vizibilitate pentru o variabilă, ci este un fel de CASE pentru tipuri (vom vedea pe parcurs).

Să trecem peste detalii şi să notăm că au dispărut instrumentele de intrare/ieşire, sarcinile acestora fiind preluate de rutine de bibliotecă. Au dispărut şi facilităţile de eliberare explicită a memoriei alocate dinamic ("dispose"), deoarece colectarea se face automat de către mediu (framework). Oarecum mai "ciudat" este faptul că nu mai există program principal. Singura unitate de compilare este modulul. Un modul "exportă" anumite elemente (constante, variabile, tipuri, proceduri), care devin astfel vizibile din modulele care-l "importă" (clienţii modulului). Important este însă că modulul este, totodată, unica unitate de încărcare. Aceasta se traduce prin faptul că, spre deosebire de Pascal, codul modulelor importate nu este inclus în codul executabil produs de compilator.

Cum se petrec de fapt lucrurile? La apelarea unei proceduri exportate de un modul, mediul (mai precis o componentă a acestuia numită Loader) va verifica mai întâi dacă modulul în cauză şi modulele importate sunt deja încărcate, după care le va încărca doar pe cele care lipsesc. După ce procedura a fost executată, modulele nu sunt descărcate. Dacă se fac corecţii în codul sursă şi se face o nouă compilare, la un nou apel se va petrece ceva (aparent) ciudat: programul va merge exact ca înainte. Evident, pentru a rula noua variantă, modulul trebuie mai întâi descărcat (explicit).

Deşi aceste ultime aspecte menţionate se leagă mai curând de mediu decât de limbaj, este important de notat că mediul joacă rolul unui supra-sistem de operare, deschis şi extensibil, care abstractizează particularităţile sistemului de operare gazdă (de pildă sistemul de fişiere, sistemul de ferestre, bucla de mesaje etc.) pentru a oferi o viziune dintre cele mai moderne (un mediu orientat pe documente). În felul acesta, programarea se îmbină şi cu o înţelegere mai profundă a paradigmelor informatice actuale.

Un prim exemplu

Pentru familiarizare, vă propun să ne jucăm puţin cu stive reprezentate prin liste înlănţuite. Există anumite uzanţe în scrierea codului, care fac apel la caractere aldine sau cursive, la folosirea culorilor, a majusculelor şi minusculelor etc. Vom scăpa deci de obişnuitele caractere monospaţiate şi vom folosi pentru cod un corp de literă neserifat. Consideraţi comentariu tot ce este scris ca text obişnuit.

     MODULE MirEx01;

Aşa cum am arătat, nu există nici PROGRAM, nici UNIT. Doar MODULE.

      IMPORT Out, Strings;

Se importă două module: Out dispune de proceduri de scriere în fereasta implicită Log (pentru testare) iar Strings dispune de proceduri de lucru cu şiruri de caractere. Interfeţele tuturor modulelor sunt publice, iar a celor uzuale sunt documentate.

      TYPE
        String = POINTER TO ARRAY OF CHAR;

În primul rând se remarcă utilizarea cuvântului cheie POINTER TO în locul semnului "^". În al doilea rând, faptul că tabloul nu are limită prestabilită (se cheamă "tablou deschis"). Se alocă dinamic, aşa cum vom vedea.

        Node = POINTER TO RECORD
          info: String;
          next: Node
        END;

Este vorba despre elementele listei care va stoca informaţia din stivă. De notat că în Oberon standard, această formulare nu este admisă (se impune utilizarea unui tip static ajutător). Deoarece practica uzuală este de a lucra cu articole alocate dinamic (vom vedea de ce), Component Pascal dispune de această variantă de declaraţie, mai scurtă şi mai expresivă.

        Stack = POINTER TO RECORD
          count: INTEGER;
          head: Node
        END;

Acesta este tipul care va reprezenta stiva. Cuprinde un contor al elementelor din stivă şi o referinţă la vârful stivei.

      VAR stack: Stack;

Iată şi o declaraţia unei variabile. Domeniul ei de vizibilitate este modulul. Spre deosebire de Pascal, alocarea ei este completată cu o iniţializare implicită (NIL pentru pointeri, zero binar pentru numere şi caractere etc.).

      PROCEDURE Push (s: Stack; info: String);
        VAR n: Node;
      BEGIN
        NEW(n); n.info := info; n.next := s.head;
        s.head := n; INC(s.count)
      END Push;

Procedura creează un nod şi îl adaugă listei elementelor, după care incrementează contorul. O observaţie: numele procedurii trebuie repetat la sfârşit. Este important de notat că dereferenţierea pointerilor (prin marcajul "^") este opţională.

      PROCEDURE Pop (s: Stack): String;
        VAR n: Node;
      BEGIN
        IF s.count # 0 THEN
          n := s.head; s.head := n.next;
          DEC(s.count);
          RETURN n.info
        ELSE RETURN NIL END
      END Pop;

Este o funcţie care returnează un pointer la un tablou de caractere. Declaraţia se face tot ca PROCEDURE (nu prin function, ca în Pascal), iar valoarea de retur se specifică prin instrucţiunea RETURN, care determină şi revenirea în procedura apelantă. Instrucţiunile care afectează fluxul execuţiei se scriu (prin convenţie) în aldine. Semnul "#" înseamnă "diferit de".

De remarcat că deşi nodul din vârful stivei este scos din uz, el nu este eliberat explicit. Din moment ce nu mai este referit, colectorul de memorie îl va elibera la un moment ulterior (nedeterminat).

      PROCEDURE Test*;
        VAR i: INTEGER; info: String;
          s: ARRAY 255 OF CHAR;
      BEGIN
        FOR i := 10 TO 1 BY -2 DO
          Strings.IntToString(i, s);
          s := "Testing " + s;
          NEW(info, LEN(s)); info^ := s$;
          Push(stack, info);
        END;
        WHILE stack.count > 0 DO
          Out.String(Pop(stack)); Out.Ln
        END
      END Test;

Procedura Test este exportată, fapt specificat prin marcarea numelui ei cu o steluţă (*). Prin convenţie, elementele exportate de un modul se scriu în aldine. Dacă nu am fi exportat această procedură, nu am fi avut în nici un fel acces la funcţionalitatea modulului.

De remarcat:

  • Schimbarea formei instrucţiunii FOR: nu mai există varianta DOWNTO, ci se foloseşte un pas negativ (e mai uniform şi mai natural);

  • Procedurile importate sunt prefixate cu numele modulului de unde provin.

  • Tablourile deschise sunt alocate dinamic, prin procedura standard NEW, unde al doilea parametru specifică dimensiunea. Funcţia standard LEN returnează lungimea şirului de caractere.

  • Şirurile de caractere sunt stacate în tablouri, iar sfârşitul caracterelor "utile" este marcat cu un zero binar (ca în C). Operatorul "$" (extensie Component Pascal) extrage dintr-un tablou caracterele utile, ceea ce permite atribuirea.

  • Variabila info a trebuit dereferenţiată explicit pentru a permite o operaţie cu şiruri de caractere (deşi stocate în tablouri, şirurile de caractere beneficiază de operaţii proprii, diferite de operaţiile cu tablouri).

          BEGIN
            NEW(stack);
            stack.head := NIL; stack.count := 0
          END MirEx01.
    

Această secvenţă seamănă cu "programul principal" din Pascal, dar reprezintă cu totul altceva. Este vorba de o secvenţă de instrucţiuni (opţională) care se va executa automat la încărcarea modulului (de regulă iniţializarea unor variabile "globale" la nivelul modulului). Există şi posibilitatea de a specifica o secvenţă (introdusă prin CLOSE) care să se execute la descărcarea modulului. Aceste variante sunt rar folosite, practica uzuală fiind ca funcţionalitatea modulului să se exprime prin elementele exportate.

Modulul se încheie cu END urmat de numele modulului.

Compilarea modului va procesa codul sursă plus fişierele de simboluri ale modulelor importate şi va produce două fişiere: un fişier care conţine codul executabil (de fapt e un fel de mic DLL) şi un fişier de simboluri, care cuprinde reprezentarea binară a interfeţei modulului (simbolurile exportate). Compilatorul îndeplineşte şi anumite sarcini care de regulă revin unui editor de legături (verifică consistenţa utilizării elementelor importate).

Desigur, modulul prezentat nu are altă utilitate decât de a pune în lumină asemănarea cu limbajul Pascal şi de a evidenţia câteva diferenţe (mai mult de ordin formal). Comanda - procedură fără parametri - exportată (Test) poate fi apelată programatic din orice modul care importă modulul MirEx01 sau poate fi apelată interactiv (se poate plasa într-un meniu, poate fi asociată unui buton dintr-o formă etc.). Rezultatul execuţiei va fi că în fereastra Log se va scrie:

      Testing 2
      Testing 4
      Testing 6
      Testing 8
      Testing 10

Obiecte

Când a conceput limbajul Oberon, Wirth s-a ferit în mod deliberat de utilizarea unui jargon obiectual, pentru a evita aşa-numitul "şoc cultural" resimţit de proiectanţi la primul contact cu lumea obiectelor. În Oberon, noţiunile obiectuale decurg în mod firesc din cele ale programării structurate. Astfel, orice articol (record) sau pointer la articol este, de fapt, un obiect. Desigur, tipurile corespunzătoare pot fi considerate echivalente cu clasele din alte limbaje obiectuale. În exemplul precedent, tipurile Node şi Stack sunt de fapt clase de obiecte.

Până aici, singura diferenţă faţă de Pascal este că articolele pot fi extinse, ceea ce corespunde în limbaj obiectual cu derivarea unei noi clase. De exemplu, pot să definesc tipul StackMsg astfel:

      StackMsg = EXTENSIBLE RECORD END;

Pe baza acestui tip pot defini apoi alte tipuri:

      PopMsg = RECORD (StackMsg) info: String END;
      PushMsg = RECORD (StackMsg) info: String END;

Câteva observaţii:

  • În Oberon standard, orice tip articol (sau pointer la articol) este în mod implicit extensibil. În Component Pascal, tipurile extensibile trebuie declarate explicit (tipurile sunt considerat implicit "finale").

  • Paranteza de după cuvântul cheie RECORD specifică tipul "părinte".

  • Tipul StackMsg este vid (nu are nici un câmp). Este folosit doar ca o bază pentru extensie.

  • Tipurile PopMsg şi PushMsg sunt diferite, deşi întâmplător au aceleaşi câmpuri.

  • Oriunde poate fi folosit un obiect dintr-o clasă de bază, poate fi folosit orice obiect aparţinând unei extensii a clasei de bază, excepţie făcând atribuirile între variabile alocate static. Totuşi, o extensie statică se poate substitui tipului de bază ca parametru de tip referinţă al unei proceduri (declarat cu VAR).

Pentru a putea vorbi de programare obiectuală, trebuie să existe posibilitatea de a lega într-o manieră formală un anumit tip de operaţiile care-i aparţin. Iniţial, această legătură se realiza în Oberon exclusiv prin intermediul câmpurilor procedurale. Iată un exemplu:

      TYPE
      Stack = POINTER TO RECORD
          count: INTEGER;
          head: Node;
          handle: Handler
      END;

Tipul Handler se poate declara astfel:

      Handler = PROCEDURE (s: Stack; VAR msg: StackMsg);

Tipurile procedurale nu figurează în standardul Pascal, dar sunt larg folosite în versiunile de la Borland, aşa că nu reprezintă o noutate (sunt oarecum analoage cu pointerii la funcţii din C). Se poate acum defini o procedură care să fie conformă declaraţiei tipului Handler. Iată o posibilitate:

      PROCEDURE MyHandler (s: Stack; VAR msg: StackMsg);
        BEGIN
          WITH msg: PopMsg DO msg.info := Pop(s)
          | msg: PushMsg DO Push(s, msg.info)
          ELSE (* nu face nimic *)
          END
        END MyHandler;

Aşa cum am menţionat, al doilea parametru de apel poate fi o extensie a tipului StackMsg, deoarece se transmite ca referinţă (implementată ca pointer). Instrucţiunea WITH este asemănătoare cu CASE, diferenţa fiind că selecţia se face în funcţie de tipul variabilei (e ceva mai mult, dar vom detalia la momentul potrivit). Instrucţiunea WITH dispune şi de o ramură (opţională) ELSE, care specifică o acţiune care să se execute atunci când tipul variabilei nu corespunde nici uneia dintre opţiuni. În exemplul de mai sus, ramura este lăsată vidă (observaţi modul de scriere a comentariilor, care pot fi imbricate), astfel încât mesajele (variabile aparţinând extensiilor tipului StackMsg) care nu sunt cunoscute nu vor provoca o eroare (vor fi ignorate).

Pentru a pune în mişcare acest mecanism, este necesar să modificăm modul de iniţializare a unei variabile de tip Stack:

      NEW(stack);
      stack.handle := MyHandler;

Am eliminat iniţializările celorlalte câmpuri (deoarece valorile coincideau cu cele implicite) şi am instalat handler-ul definit mai sus. Din acest moment, operaţiile Push şi Pop pot fi realizate trimiţând obiectului stack mesaje de tip PushMsg sau PopMsg. Dacă pushMsg este o variabilă de tip PushMsg, atunci vom pune în câmpul info informaţia pe care vrem să o stocăm în stivă după care vom trimite stivei mesajul astfel:

      stack.handle(stack, pushMsg)

Pentru a extrage din stivă o informaţie, nu trebuie decât să trimitem stivei un mesaj de tipul PopMsg, după care vom găsi în câmpul info informaţia extrasă:

      stack.handle(stack, popMsg)

Această tehnică este o implementare ad literam a terminologiei obiectuale: se trimite un mesaj unui obiect (deşi în limbajele obiectuale pure trimiterea unui mesaj înseamnă, de fapt, invocarea unei metode). Deşi uşor redundantă ca expresie (receptorul mesajului este specificat şi ca argument), această implementare a obiectelor s-a dovedit foarte eficientă: prima implementare a sistemului de operare Oberon s-a bazat în întregime pe această tehnică şi a însemnat doar 12.000 de linii de cod.

Este interesant de remarcat că putem să schimbăm dinamic comportamentul unui obiect, instalând un alt handler. În plus, dacă exportăm tipurile necesare, este posibil să definim în modulele client noi tipuri de mesaje. Pentru a le trata, vom defini un nou Handler, care va face apel (printr-un aşa-numit super call) la handler-ul iniţial. Oricât de tentate ar fi, aceste tehnici sunt nerecomandabile şi au fost păstrate în versiunea revizuită a limbajului doar pentru compatibilitate cu versiunea iniţială.

Metode

Există însă în Oberon o modalitate mult mai directă şi mai expresivă de a lega explicit anumite procesări de un anume tip: procedurile tipizate (type bound procedures). Revenind la exemplul iniţial, procedura Push ar putea fi scrisă astfel:

      PROCEDURE (s: Stack) Push (info: String), NEW;
        VAR n: Node;
      BEGIN
        NEW(n); n.info := info; n.next := s.head;
        s.head := n; INC(s.count)
      END Push;

Sintactic, diferenţa este că argumentul s de tip Stack a trecut într-o paranteză plasată înaintea numelui procedurii (despre marcajul NEW vom vorbi mai târziu). Semantic, diferenţa este că procedura aparţine acum tipului Stack şi, în consecinţă, nu poate fi aplicată decât obiectelor de acest tip. Apelul procedurii se va face astfel:

      stack.Push(info)

Nu e greu de observat că procedurile stocate sunt, de fapt, metode veritabile. Aparţinând unui tip, ele vor fi moştenite de extensiile tipului, vor putea fi eventual extinse sau suprascrise. Însă pentru a nu fi suprascrise din greşeală, atunci când sunt introduse trebuie marcate cu NEW (e o extensie Component Pascal). Mai mult, Component Pascal impune declararea explicită a metodelor care pot fi extinse, astfel încât proiectantul este forţat să gândească arhitectura aplicaţiei înainte de a scrie cod. În plus, deciziile arhitecturale se exprimă chiar în limbajul de programare, astfel încât siguranţa întregului ansamblu poate fi sporită.

Parametrul formal s este echivalentul lui self din Smalltalk sau al lui this din Java. Şi în această privinţă creatorii limbajului s-au ferit să introducă un concept nou, preferând să se bazeze pe o noţiune simplă (parametri de procedură).

O particularitate a modelului obiectual din Oberon este că nu există constructori (metode specializate pentru crearea şi iniţializarea instanţelor unei clase) şi, cu atât mai puţin, destructori. Există în schimb un set de modalităţi mult mai flexibile prin care se pot realiza aceste operaţii.

Tipuri de tipuri

Înainte de a prezenta un exemplu mai complex, trebuie să clarific unele aspecte legate de implementarea obiectelor în Oberon.

Un prim aspect se referă la diferenţa dintre obiectele alocate static şi cele alocate dinamic. Primele aparţin tipurilor care se descriu prin articole (RECORD) iar declararea unei variabile de acest tip are ca efect alocarea de către compilator a unei zone de memorie pe stivă. În afară de câmpurile propriu-zise, compilatorul alocă şi un câmp ascuns (inaccesibil programatorului) în care este stocată o referinţă la un descriptor de tip. Acesta reprezintă o structură care cuprinde, printre altele, referinţele la metodele tipului în cauză precum şi o referinţă la descriptorul tipului "părinte" (din care a fost derivat).

Variabilele cu alocare dinamică aparţin unor tipuri descrise ca pointer la articol iar declararea unei variabile de acest tip are ca efect alocarea în stivă doar a unei zone de memorie corespunzătoare unui pointer, iniţializată automat cu NIL. Alocarea propriu-zisă se realizează cu procedura standard NEW şi are ca efect rezervarea unei zone corespunzătoare articolului în memoria dinamică (heap) şi iniţializarea ei cu valori implicite (NIL pentru pointeri, zero binar în rest). Abia aici se alocă şi se încarcă referinţa la descriptorul de tip. Astfel se explică absenţa constructorilor (înainte de alocarea dinamică, obiectul nu are acces la descriptorul de tip, deci nu are metode).

Anumite tipuri (cu alocare statică sau dinamică) sunt extensibile. Tipurile abstracte (marcate prin ABSTRACT) pot fi extinse, dar nu pot fi alocate. Tipurile declarate EXTENSIBLE pot fi extinse şi pot fi alocate. Tipurile declarate LIMITED pot fi extinse şi alocate doar în modulul în care sunt definite (chiar dacă sunt exportate).

Între tipurile cu alocare dinamică există o regulă simplă de compatibilitate: un obiect a de tip T este compatibil cu un obiect b care aparţine tipului T sau unei extensii a acestuia. Această compatibilitate înseamnă că o atribuire a := b este legală (dat nu şi reciproc). Dacă T este un tip cu alocare statică, atribuirea nu ar fi legală.

Datorită acestei compatibilităţi, orice obiect are un tip static (cel declarat) şi un tip dinamic (care poate coincide cu cel static sau poate fi o extensie a acestuia). Dacă variabila a aparţine tipului T0 iar b aparţine tipului T1 (care este extensie a lui T0) atunci în urma atribuirii (legale) a := b tipul dinamic al obiectului a devine T1, în timp ce tipul static este în continuare T0.

Pentru obiectele alocate static, tipul dinamic coincide întotdeauna cu tipul static. Dacă o procedură acceptă ca parametru de tip referinţă (declarat cu VAR) o variabilă aparţinând unui tip cu alocare statică T, compilatorul generează un pointer la articolul furnizat ca parametru actual, aşa încât în interiorul procedurii tipul T este considerat cu alocare dinamică.

Tipul static al unui obiect determină care sunt câmpurile accesibile din program. Dacă tipul dinamic al unui obiect nu coincide cu tipul static, singura posibilitate de a accede la câmpurile adăugate prin extensie este de a folosi un "gardian de tip" (type guard). Iată un exemplu:

      TYPE
        T0 = POINTER TO EXTENSIBLE RECORD
          alfa: INTEGER
        END;
        T1 = POINTER TO RECORD (T0)
          beta: REAL
        END;
      VAR a: T0; b: T1; r: REAL;
        ...
        a := b;
        r := a(T1).beta; (* type guard *)
        ...

Sintactic, gardianul de tip reprezintă o paranteză cu numele tipului dinamic, plasată după variabila căreia i se aplică. Semantic, gardianul de tip schimbă tipul static al variabilei. Totodată, gardianul de tip reprezintă şi o aserţiune: tipul dinamic al obiectului este cel indicat (în exemplu, T1). Dacă nu este aşa, se produce o eroare la execuţie. Cu alte cuvinte, aplicarea unui type guard se face pe responsabilitatea programatorului. Din acest motiv, de regulă, aplicarea unui gardian de tip se face sub controlul unui test de tip:

      IF a IS T1 THEN r := a(T1).beta END;

Instrucţiunea WITH combină un set de teste de tip cu gardienii de tip corespunzători (vezi procedura MyHandler din secţiunea precedentă).

Tipul dinamic al unui obiect este folosit pentru legarea dinamică (late binding) a metodelor. Mecanismul este cel clasic: invocarea unei metode face apel la descriptorul de tip, în care se caută metoda respectivă. Dacă nu este găsită aici, se caută între metodele tipului "părinte" şi aşa mai departe. Pentru tipurile cu alocare statică legarea se face la momentul compilării.

Liste generice

S-ar putea ca cititorilor familiarizaţi cu programarea obiectuală în C++ sau Java, modelul obiectual din Component Pascal să li se pară destul de sărăcăcios şi inflexibil. Lucrurile nu stau însă aşa. Există o multitudine de posibilităţi de a controla în detaliu mecanismele de încapsulare, moştenire şi extensie, astfel încât invarianţii specifici tipurilor să nu poată fi afectaţi de clienţi.

Deoarece articolul acesta nu îşi propune să vă facă doctori în Oberon, ci doar să vă sugereze posibilităţile acestuia, voi recurge din nou la un exemplu. Ideea de bază este următoarea: să scriem un modul care să furnizeze clienţilor un mecanism generic pentru liste înlănţuite, astfel încât acesta să poată fi folosit pentru orice tip de informaţii.

Modalitatea de reprezentare este preluată din Lisp. O listă este de fapt un arbore binar, fiecare nod al acestuia având un pointer (numit car) către informaţia utilă a nodului şi un pointer (numit cdr) către restul listei.

Modulul va trebui să garanteze următoarele proprietăţi ("invarianţi"): Pointerul car nu va fi niciodată NIL iar nodul al cărui pointer cdr este NIL va fi ultimul din listă. O listă vidă se reprezintă printr-un arbore cu un singur nod, având ambii pointeri NIL.

      MODULE MirLists;

        CONST equal* = 0; less* = -1; greater* = 1; different* = 9;

În Pascal, am fi reprezentat aceste valori printr-un tip enumerat. În Oberon, folosim constante întregi. Modulele client le vor folosi pe baza semnificaţiei lor (care reiese clar din numele alese, dar se poate explicita în documentaţia interfeţei).

        TYPE
          Atom* = POINTER TO ABSTRACT RECORD END;
          List* = POINTER TO ABSTRACT RECORD (Atom)
            car: Atom
          END;

Tipul Atom este doar o bază pentru extensie. Fiind declarat ABSTRACT, el va putea fi extins în acest modul sau în modulule client (observaţi că este exportat), deoarece orice tip abstract este extensibil. Însă nu va putea fi alocat prin procedura standard NEW. În schimb, extensiile sale concrete (sau "finale") definite în modulele client vor putea fi instanţiate.

Tipul List este o extensie a tipului Atom, ceea ce ne va permite imbricăm listele (un element al unei liste poate fi o altă listă). Şi acest tip este abstract, deci clienţii acestui modul îl vor putea eventual extinde, dar nu-l vor putea aloca. Observaţi că singurul câmp al articolului este car şi nu este exportat, ceea ce evidenţiază încapsularea: clienţii nu vor avea acces la acest câmp, nici măcar nu vor ştii că există.

Vă puneţi cu siguranţă două întrebări: unde este celălalt câmp al reprezentării (cdr) şi, mai ales, ce vor putea face clienţii cu aceste tipuri, din moment ce nu pot crea instanţe?

Răspunsurile se profilează în următoarea declaraţie de tip:

        StdList = POINTER TO RECORD (List)
          cdr:StdList
        END;

Tipul StdList este o extensie concretă a tipului abstract List, ceea ce înseamnă că (1) moşteneşte câmpurile (în speţă car) şi metodele (le vom vedea mai târziu) tipului de bază, şi (2) se pot aloca variabile de acest tip. Dar, pentru că tipul nu este exportat, alocarea variabilelor se poate face doar în acest modul.

Într-un fel, vom păcăli clienţii: le vom pune la dispoziţie o funcţie care să instanţieze tipul List, dar le vom furniza de fapt instanţe ale tipului concret StdList.

De fapt, tipul StdList reprezintă implementarea tipului abstract List. Este o convenţie ca numele tipurilor concrete utilizate pentru implementare să fie prefixate cu "Std" (de la "standard").

Observaţie: Puteam să plasăm ambii pointeri (car şi cdr) în tipul StdList sau în tipul List (dar fără să-i exportăm). Important este doar ca aceste câmpuri să fie "ascunse" clienţilor, pentru a încapsula în acest modul întregul mecanism al înlănţuirii (protejând astfel invarianţii specifici tipului List).

Ce se întâmplă însă cu tipul Atom? Acest tip va rămâne în responsabilitatea clientului, care va trebui să-l extindă, pentru a purta o informaţie utilă, la alegerea sa. Deocamdată el poate reprezenta doar liste. Mai mult, clientul va putea deriva mai multe tipuri concrete, fiecare păstrând compatibilitatea cu listele generice importate din acest modul (altfel spus: listele pot fi eterogene).

Totuşi, modulele client vor trebui să ofere o funcţionalitate minimală extensiilor tipului Atom. Pentru aceasta vom defini aici câteva metode:

      PROCEDURE (a: Atom) Clone* (): Atom, NEW, ABSTRACT;
      PROCEDURE (a: Atom) Compare* (b: Atom): INTEGER, NEW, ABSTRACT;
      PROCEDURE (a: Atom) Prel*, NEW, EMPTY;

Primele două metode sunt declarate abstracte. O metodă abstractă nu are corp iar invocarea ei se soldează cu o eroare. Orice tip care extinde tipul de bază, pentru a fi concret (deci alocabil) va fi obligat să suprascrie (override) aceste metode cu metode concrete.

Metoda de clonare este necesară deoarece majoritatea operaţiilor asupra listelor vor lucra cu copii ale listelor (listele sunt imutabile). Clonarea listelor nu se poate însă realiza în absenţa unei modalităţi de a clona atomi.

Metoda de comparare este necesară pentru a implementa metode de căutare a unui atom într-o listă. La modul minimal, această metodă va trebui să returneze constantele equal sau different (exportate de acest modul).

Interesantă este metoda Prel, cere este declarată "vidă" (EMPTY). Metodele vide reprezintă interfeţe opţionale. O metodă vidă (spre deosebire de una abstractă) poate fi invocată. Apelul nu va produce o eroare, dar nu va realiza nici o prelucrare. Aşadar, implementarea unei metode vide este opţională.

Urmează acum o funcţie (nu este metodă!) pe care clienţii o vor putea folosi pentru a crea instanţe ale tipului List:

      PROCEDURE New* (car, cdr: Atom): List;
        VAR l: StdList;
      BEGIN
        ASSERT( (car=NIL) & (cdr = NIL) OR (car # NIL), 21);
        NEW(l); l.car := car;
        IF cdr = NIL THEN l.cdr := NIL
        ELSIF cdr IS StdList THEN l.cdr := cdr(StdList);
        ELSE NEW(l.cdr); l.cdr.car := cdr; l.cdr.cdr := NIL
        END;
        RETURN l
      END New;

De fiecare dată când un client va apela această funcţie (este ceea ce se cheamă factory function) va primi o instanţă o tipului StdList (dar acest lucru este transparent). Altfel spus, în modulele client obiectele de tip List vor avea tipul dinamic StdList.

Este interesant să remarcăm că funcţia stabileşte o precondiţie printr-o aserţiune. Procedura standard ASSERT primeşte ca prim argument o expresie logică. Dacă această condiţie nu este îndeplinită, programul se opreşte şi produce un cod de eroare, a cărei valoare este furnizată de al doilea argument (de tip INTEGER). Precondiţia cere ca fie ambele argumente să fie NIL, fie primul argument să fie diferit de NIL.

Dacă ambele argumente sunt NIL se va construi o listă vidă. Dacă se furnizează doar primul argument (al doilea fiind NIL), se va produce o listă având un singur element (atomul furnizat ca prim argument). Dacă şi al doilea argument este diferit de NIL, atunci există două posibilităţi: fie acesta este o listă (caz în care se produce lista respectivă, în faţă căreia s-a adăugat elementul furnizat ca prim argument), fie este un atom (caz în care se produce o listă având ca elemente cei doi atomi furnizaţi).

Iată şi câteva metode pentru List:

      PROCEDURE (l: List) Clone* (): List, ABSTRACT;
      PROCEDURE (l: List) FPut* (a: Atom): List, NEW, ABSTRACT;
      PROCEDURE (l: List) LPut* (a: Atom): List, NEW, ABSTRACT;
      PROCEDURE (l: List) First* (): Atom, NEW, ABSTRACT;
      PROCEDURE (l: List) ButFirst* (): List, NEW, ABSTRACT;

Observaţi că metoda Clone nu are marcajul NEW. Explicaţia este că tipul de bază pentru List este Atom, care dispune de o metodă Clone. Aşadar, această metodă suprascrie metoda moştenită de la Atom. Semnificaţia metodelor este clară: Clone va produce o copie a listei, FPut va returna o copie a listei la care s-a adăugat ca prim element un atom, LPut este similară cu FPut cu diferenţa că atomul se adaugă la sfârşitul listei, First va returna o copie a primului element al listei în timp ce ButFirst va returna o copie a listei fără primul element.

Metodele sunt abstracte, deci nu vor putea fi invocate. Atunci la ce folosesc? Ca bază pentru extensie. Ele vor fi suprascrise de metodele corespunzătoare ale tipului StdList, aşa cum vom vedea imediat. Până atunci, să mai remarcăm că un tip concret nu poate avea metode abstracte, dar un tip abstract poate avea metode concrete. Iată un exemplu:

      PROCEDURE (l: List) IsEmpty* (): BOOLEAN, NEW;
      BEGIN
        IF (l.car = NIL) & ( l(StdList).cdr = NIL) THEN RETURN TRUE
        ELSE RETURN FALSE END
      END IsEmpty;

Se remarcă şi aici folosirea gardianului de tip pentru variabila l, care are tipul static List. Datorită funcţiei New (definite mai sus) putem fi siguri că tipul dinamic al listei este StdList, deci putem aplica acest type guard fără precauţii suplimentare.

Iată şi promisa implementare a câtorva metode ale tipului List prin metode concrete ale tipului StdList:

      PROCEDURE (l: StdList) Clone (): StdList;
        VAR p: StdList;
      BEGIN
        NEW(p);
        IF ~l.IsEmpty() THEN
          p.car := l.car.Clone();
          IF l.cdr # NIL THEN p.cdr := l.cdr.Clone() END;
        END;
        RETURN p
      END Clone;

Să remarcăm mai întâi covarianţa tipurilor. Metoda suprascrie o metodă (abstractă) a tipului List care returnează o valoare de tip List (un pointer). În Component Pascal este legal ca metoda sub-tipului să returneze o instanţă a sub-tipului.

În al doilea rând, să remarcăm că metoda face apel la metoda Clone a tipului Atom. Acest lucru este extrem de spectaculos şi reprezintă cheia extensibilităţii întregului framework. Deoarece tipul Atom este abstract, nu se vor putea instanţia decât variabile ale unui tip concret pe care clienţii modulului îl vor defini ca o extensie a tipului Atom. Pentru a defini însă acest tip, clienţii vor trebui să implementeze metoda abstractă Clone moştenită. Cu alte cuvinte, această metodă apelează un cod care încă nu există!

În rest, codul este simplu, recursivitatea fiind tipică pentru structuri arborescente. Poate ar mai trebui accentuată importanţa polimorfismului: dacă un element al listei (l.car) este o listă, atunci apelul l.car.Clone() va determina (datorită legării dinamice) apelarea recursivă a acestei metode, altfel se va apela metoda Clone a extensiei tipului Atom definită în modulul client (ceea ce se cheamă up call).

Iată încă o metodă a tipului StdList care suprascrie o metodă abstractă a tipului List:

      PROCEDURE (l: StdList) FPut (a: Atom): StdList;
        VAR p: List;
      BEGIN
        ASSERT (a # NIL, 21);
        IF l.IsEmpty() THEN p := New(a.Clone(), NIL)
        ELSE p := New(a.Clone(), l.Clone()) END;
        RETURN p(StdList)
      END FPut;

Implementarea face uz de funcţia New definită mai sus şi de metoda de clonare pentru atomi şi liste, după ce s-a asigurat că atomul primit nu este NIL.

      PROCEDURE (l: StdList) First (): Atom;
      BEGIN
        ASSERT (~l.IsEmpty(), 21);
        RETURN l.car.Clone()
      END First;

Se pot inventa nenumărate metode pe baza acestui procedeu: se declară metoda abstractă pentru tipul List şi se implementează prin metoda corespunzătoare a tipului StdList. Codul este banal.

Important este efectul: utilizatorul va crede că lucrează cu obiecte de tip List, cărora le va aplica metodele importate (abstracte). Deoarece tipul dinamic al acestor obiecte este StdList (concret şi invizibil din modulele client), metodele invocate de fapt sunt - datorită legării dinamice - metodele corespunzătoare ale tipului StdList.

În felul acesta am separat interfaţa (partea abstractă) de implementare (partea concretă) fără să utilizăm o construcţie specială, aşa cum se procedează în Java. Dacă tot facem comparaţii, să notăm că în Smalltalk nu se face nici o separaţie între interfaţă şi implementare. :

      (* celelalte metode *)
      END MirLists.

Pentru a economisi spaţiul tipografic, nu mai furnizăm celealte metode. Veţi găsi pe situl Web al revistei (www.pcreport.ro) o implementare completă.

Interfeţe

Modulul MirLists furnizează o abstracţie: o structură de listă care încapsulează mecanismul înlănţuirii. Utilizatorul nu va mai trebui să se ocupe de detaliile implementării, ci va importa modulul MirList şi se va încrede în contractul reprezentat de interfaţa acestuia. Un browser specializat al mediului de lucru i-o poate furniza în orice moment (vezi caseta Interfeţele sunt contracte).

Această abordare are câteva avantaje extrem de importante. În primul rând, se evită proliferarea unor implementări ad-hoc pentru abstracţii utile mai multor utilizatori. Într-o aplicaţie de mari dimensiuni se folosesc sute de liste înlănţuite şi pentru fiecare dintre ele se implementează toate operaţiile necesare. Acest lucru conduce la o încărcare inutilă a codului şi, mai ales, sporeşte riscul apariţiei unor erori ascunse. Textele reprezintă un exemplu similar: gândiţi-vă câte aplicaţii Windows implementează (fiecare în felul ei) procesarea textelor şi imaginaţi-vă ce ar însemna ca sistemul de operare să furnizeze o abstracţie (un text generic împreună cu operaţiile tipice) care apoi să poată fi specializată de aplicaţii în funcţie de propriile nevoi.

Un alt aspect îl reprezintă depanarea. Dacă - în ciuda măsurilor prin care Oberon le ia pentru a spori siguranţa sistemului (tipizare strictă, colectare automată a memoriei etc.) - apare totuşi o "scamă" (bug), localizarea ei este extrem de simplă, deoarece încapsularea previne interferenţele necontrolate.

În fine, un alt aspect extrem de important: implementarea unei abstracţii poate fi schimbată (de pildă, din motive de performanţă) fără ca aceasta să afecteze clienţii respectivei abstracţii. Condiţia este ca interfaţa să fie păstrată sau, eventual, extinsă (deci: contractul să fie respectat). Aţi avut vreodată necazuri cu DLL-urile din Windows?

Pentru a obţine aceste avantaje, Oberon se bazează pe un principiu major de proiectare: Evitarea exportului de tipuri concrete.

Aceasta înseamnă, de fapt, că se evită moştenirea implementării şi se încurajează moştenirea interfeţei. Deoarece tehnica uzuală este de a exporta tipuri abstracte, crearea şi iniţializarea instanţelor este sub controlul exclusiv al modului exportator, care poate garanta păstrarea invarianţilor specifici. În plus, moştenirea implementării poate conduce la efecte imprevizibile în cazul în care se operează o modificare în program.

Un corolar al acestui principiu este că se favorizează crearea noilor tipuri prin compoziţie (nu prin derivare). Deoarece tipul de bază este un fel de "cutie neagră", compoziţia este calea cea mai naturală de construi noi abstracţii. Pentru aplicaţii concrete, tipurile furnizate de framework sunt, în principiu, mai mult decât suficiente.

Înapoi la stivă

Pentru a ilustra compoziţia tipurilor, să încercăm să schiţăm un modul care să furnizeze o stivă care să lucreze cu listele înlănţuite importate din modulul MirLists.

      MODULE MirStacks;

        IMPORT L := MirLists;

Este un import simplu al modulului MirLists, dar prin această formulare vom putea folosi numele L ca un sinonim pentru MirLists (e mai comod). :

      CONST equal* = 0; different* = 9;

Nu are sens să vorbim despre "stive ordonate", deci aceste constante sunt suficiente. :

      TYPE
        Stack* = POINTER TO ABSTRACT RECORD
          l: L.List;
          count-: INTEGER;
        END;

Iată şi compoziţia! În loc să definim stiva ca o extensie a listei, am preferat să o compunem utilizând o listă (ascunsă) pentru stocarea informaţiei (l) şi un contor al elementelor (count). Acest contor nu este ascuns, ci este exportat read-only (asta reprezintă marcajul semnul minus).

        Atom* = L.Atom;

Utilizatorii stivelor vor avea nevoie de tipul abstract Atom pentru a defini tipurile de informaţie pe care le vor stoca în stivă. Nu facem decât să facem vizibil tipul definit în modulul MirLists.

        StdStack = POINTER TO RECORD (Stack) END;

Acesta este tipul concret care va servi ca implementare pentru Stack.

      PROCEDURE (s: Stack) Push* (a: L.Atom), NEW, ABSTRACT;
      PROCEDURE (s: Stack) Pop* (): L.Atom, NEW, ABSTRACT;
      PROCEDURE (s: Stack) IsEmpty* (): BOOLEAN, NEW;
      BEGIN RETURN s.count = 0 END IsEmpty;

Acestea sunt metodele. Observaţi că IsEmpty este o implementată direct. Deoarece nu mai este extensibilă, putem fi sigură că nu va putea fi scrisă de clienţi, deşi este exportată.

Observaţie: există posibilitatea unui export special pentru metode concrete, anume export pentru implementare. Permite o formă controlată de moştenire a implementării.

Implementarea propriu zisă este absolut banală:

      PROCEDURE New* (): Stack;
        VAR s: StdStack;
      BEGIN
        NEW(s); s.l := L.New(NIL, NIL); s.count := 0; RETURN s
      END New;

      PROCEDURE (s: StdStack) Push (a: Atom);
      BEGIN
        s.l := s.l.FPut(a); INC(s.count)
      END Push;

      PROCEDURE (s: StdStack) Pop (): Atom;
        VAR p: L.Atom;
      BEGIN
        ASSERT ( ~s.l.IsEmpty(), 21);
        DEC (s.count);
        p := s.l.First(); s.l := s.l.ButFirst();
        RETURN p
      END Pop;

      END MirStacks.

În listingul Stivuiţi în voie puteţi vedea un modul care utilizează stivele generice pentru a stoca şi a extrage cuvinte şi simboluri. Este la rându-i trivial, dar ilustrează "concretizarea" atomilor.

Observaţie finală

Poate v-aţi întrebat de ce a fost ales pentru acest limbaj numele "Oberon". Este numele unui satelit natural al planetei Uranus, descoperit în 1787 de către Sir William Herschel exclusiv prin mijloace matematice (adică: fără să fie observat vizual).


 

(Publicat în PC Report 91 - aprilie 2000)

 

Copyright © 2000 Agora Media

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