Copyright © Peter Seibel
Traducere © Razvan Popa
4. Sintaxa si Semantica
Dupa turul initial, ne vom domoli pentru cateva capitole si ne vom uita mai sistematic la capabilitatile vazute pana acum. Voi incepe cu o imagine de ansamblu a elementelor de baza din sintaxa si semantica Lisp-ului, ceea ce inseamna ca trebuie, desigur, sa raspund la o anume intrebare arzatoare. . .(pe scurt ▼)
Ce-i cu Toate Parantezele?
Sintaxa lui Lisp este destul de diferita de cea a limbajelor descendente din Algol. Cele mai evidente caracteristici sunt folosirea din abundenta a parantezelor si notatia cu prefix. Din diverse motive multi sunt alungati de sintaxa. Detractorii lui Lisp tind sa descrie sintaxa ca fiind "ciudata" si "enervanta". Lisp, spun ei, trebuie ca inseamna Legiuni de Iritante si Sacaitoare Paranteze (traducere aproximativa din Lots of Irritating
Superfluous Parentheses). Programatorii Lisp, pe de alta parte, tind sa considere sintaxa ca fiind una din virtutile limbajului. Cum e posibil ca ceva atat de agasant pentru un grup sa fie sursa bucuriei altului?
Nu pot sa apar sintaxa Lisp-ului pana nu va voi fi explicat macro-urile mai in detaliu, dar pot sa incep cu o bucatica de istorie care sugereaza ca merita sa pastrezi o minte deschisa: cand John McCarthy a inventat Lisp, el intentiona sa implementeze o sintaxa mai asemanatoare cu Algol, ceva ce el numea expresii M. Dar nu a ajuns sa o faca niciodata, si a explicat de ce in articolul "Istoria Lisp-ului" (History of
Lisp).1
Proiectul de definire a expresiilor M precis si compilarea sau macar traducerea lor in expresii S nu a fost finalizat dar nici abandonat explicit. Doar a fost amanat in viitorul indefinit, apoi a aparut o generatie noua de programatori care au preferat [expresiile S] oricarei notatii similare FORTRAN sau ALGOL ce ar fi putut fi inventata.Cu alte cuvinte, oamenilor care au folosit Lisp in ultimii 45 de ani chiar le-a placut sintaxa si au descoperit ca aceasta face limbajul mai puternic. In capitolele urmatoare vei incepe sa-ti dai seama de ce.
Deschiderea Cutiei Negre
Inainte sa ne uitam la caracteristicile sintaxei si semanticii Lisp, merita sa ne uitam la cum sunt definite si care sunt diferentele fata de alte limbaje.
In majoritatea limbajelor de programare, procesorul limbajului - indiferent daca interpretor sau compilator - opereaza ca o cutie neagra: indesi o secventa de caractere reprezentand textul programului in cutia neagra si ea executa comportamentele indicate daca e interpretor sau produce o versiune compilata a programului care va executa comportamentele cand e rulata daca e compilator.
In cutia neagra, procesoarele de limbaj sunt, desigur, impartite in subsisteme care sunt fiecare responsabile de cate o sarcina din traducerea textului in comportamente sau cod obiect. De multe ori sunt trei faze, fiecare legandu-se la urmatoarea: un analizor lexic sparge fluxul de caractere in token-uri si le trimite unui parser care construieste un arbore reprezentand expresiile din program, conform gramaticii limbajului. Acest arbore, numit arbore de sintaxa abstracta este apoi transmis unui evaluator care ori il interpreteaza direct ori il compileaza in alt limbaj, cum ar fi cod masina. Deoarece procesorul limbajului e o cutie neagra, structurile de date folosite de procesor, ca token-urile si arborii de sintaxa abstracta, conteaza numai pentru implementatorul limbajului.
In Common Lisp lucrurile sunt un pic diferite, cu consecinte atat pentru implementator cat si pentru cum este definit limbajul. In loc de o singura cutie neagra care trece de la text la comportamente de program intr-un singur pas, Common Lisp defineste doua cutii negre, una care traduce textul in obiecte Lisp si alta care implementeaza semantica limbajului in functie de aceste obiecte. Prima cutie se numeste reader si a doua evaluator.2
Fiecare cutie neagra defineste un nivel de sintaxa. Reader-ul defineste cum se pot transforma siruri de caractere in obiecte Lisp numite expresii S.3 Deoarece sintaxa de expresii S include sintaxa pentru liste sau obiecte arbitrare, inclusiv alte lista, expresiile S pot reprezenta expresii arborescente arbitrare, la fel ca arborii abstracti de sintaxa generati de parser-ele pentru limbaje non-Lisp.
Evaluatorul defineste o sintaxa de forme Lisp care pot fi construite din expresii S. Nu toate expresiile S sunt forme Lisp legale, la fel cum nu toate secventele de caractere sunt expresii S legale. De exemplu, atat
(foo 1 2)
cat si ("foo" 1
2)
sunt expresii S, dar numai prima poate fi forma Lisp deoarece o lista care incepe cu un string nu are inteles ca forma Lisp. Aceasta delimitare a cutiei negre are cateva consecinte. Una este ca poti folosi expresiile S, dupa cum ai vazut in Capitolul 3, ca format externalizabil de date utilizabil pentru alte date decat codul sursa, folosind
READ
pentru a-l citi si PRINT
pentru a-l afisa.4 Cealalta consecinta este ca deoarece semantica limbajului este definita ca arbori de obiecte si nu ca siruri de caractere, este mai usor sa se genereze cod in limbaj decat daca ar fi trebuit sa se genereze cod ca text. Generarea codului de la zero este numai putin mai usor - construirea listelor sau a sirurilor de caractere necesita cam la fel de multa munca. Beneficiul real este ca se poate genera cod prin manipularea datelor existente. Aceasta e baza macro-urilor Lisp, despre care voi discuta mai detaliat in capitole viitoare. Deocamdata ma voi concentra pe cele doua nivele de sintaxa definite de Common Lisp: sintaxa expresiilor S inteleasa de reader si sintaxa formelor Lisp inteleasa de evaluator. Expresii S
Elementele de baza ale expresiilor S sunt listele si atomii. Listele sunt delimitate de paranteze si pot contine orice numar de elemente separate prin spatii. Atomii sunt toate celelalte.5 Elementele listelor sunt ele insele expresii S - cu alte cuvinte atomi sau liste incluse. Comentariile - care nu sunt, tehnic vorbind, expresii S - incep cu ";", se intind pana la sfarsitul liniei si sunt tratate ca spatiu gol.
Si cam asta e. Deoarece listele sunt atat de triviale dpdv sintactic, singurele reguli sintactice pe care trebuie sa le stii sunt cele care reguleaza forma diverselor tipuri de atomi. In aceasta sectiune voi descrie regulile pentru cele mai utilizate tipuri de atomi: numere, siruri de caractere si nume. Apoi, voi arata modul in care expresiile S compuse din aceste elemente pot fi evaluate ca forme Lisp.
Numerele sunt destul de simple: orice secventa de cifre - posibil precedata de un semn (
+
sau -
), continand un punct decimal (.
) sau un slash (/
), sau care se termina cu un exponent - este citit ca numar. De exemplu: 123 ; numarul intre o suta douazeci si trei 3/7 ; raportul trei septimi 1.0 ; numarul unu in virgula mobila cu precizie implicita 1.0e0 ; alt mod de a scrie acelasi numar 1.0d0 ; numarul real unu cu precizie "dubla" 1.0e-4 ; echivalentul real al unei zecimi de miime +42 ; numarul intreg patruzeci si doi -42 ; numarul intreg negativ patruzeci si doi -1/4 ; raportul negativ un sfert -2/8 ; alt mod de a scrie raportul negativ un sfert 246/2 ; alt mod de a scrie intregul o suta douazeci si treiAceste forme diferite reprezinta mai multe tipuri de numere: intregi, rationale si in virgula mobila. Lisp suporta si numere complexe, care au notatia proprie si despre care voi vorbim in Capitolul 10.
Dupa cum se poate vedea din exemple, se poate nota un numar in mai multe feluri. Dar indiferent de cum e scris, toate numerele rationale - atat intregi cat si rapoarte - sunt reprezentate intern in forma "simplificata". Cu alte cuvinte, obiectele care reprezinta -2/8 sau 246/2 nu sunt diferite de obiectele care reprezinta -1/4 si 123. La fel,
1.0
si 1.0e0
sunt doar moduri diferite de a scrie acelasi numar. Pe de alta parte, 1.0
, 1.0d0
si 1
pot sa indice obiecte diferite deoarece reprezentarile in virgula mobila si numerele intregi sunt tipuri diferite. Vom lasa detaliile despre caracteristicile diverselor tipuri de numere pentru Capitolul 10.String-urile, dupa cum ai vazut in capitolul anterior, sunt incadrate cu ghilimele. Intr-un sir de caractere un backslash (
\
) "scapa" urmatorul caracter, fortandu-l sa fie inclus in sir indiferent de natura sa. Singurele doua caractere care trebuie "scapate" intr-un string sunt ghilimelele si backslash. Orice alt caracter poate fi introdus in sir fara backslash, indiferent de intelesul lui in afara sirului. Cateva exemple de siruri de caractere: "foo" ; sirul continand caracterele f, o si o. "fo\o" ; acelasi sir "fo\\o" ; sirul continan caracterele f, o, \ si o. "fo\"o" ; sirul continand caracterele f, o, " si o.Numele folosite in programele Lisp, ca
FORMAT
si hello-world
si *db*
sunt reprezentate prin obiecte numite simboluri. Reader-ul nu stie cum o sa fie folosit un nume - daca e nume de variabila, functie sau altceva. El citeste o secventa de caractere si construieste un obiect pentru a reprezenta numele.6 Aproape orice caracter poate sa apara intr-un nume. Spatiile albe nu, totusi, deoarece ele sunt separatoarele dintre elementele listelor. Cifrele pot aparea in nume atata timp cat numele ca intreg nu poate fi intrepretat ca numar. La fel, numele pot contine puncte, dar reader-ul nu poate citi un nume care are numai puncte. Zece alte caractere servesc unor scopuri sintactice si nu pot aparea in nume: parantezele rotunde, apostrof si ghilimele, "`" (apostrof invers?), virgula, doua puncte, punct si virgula, backslash si bara verticala. Si chiar si aceste caractere pot aparea, daca vrei sa le "scapi" precedandu-le cu un backslash sau inconjurand partea respectiva din sir cu bare verticale. Doua caracteristici importante ale modului in care reader-ul traduce numele in obiecte simbol au legatura cu felul in care sunt tratate marimea literelor din nume (litera mare/mica) si felul in care se asigura ca acelasi nume este citit totdeauna ca acelasi simbol. In timp ce citeste nume, reader-ul converteste toate caracterele ne-"evadate" la echivalentele lor majuscule. Deci reader-ul va citi
foo
, Foo
si FOO
ca acelasi simbol: FOO
. Dar, \f\o\o
si |foo|
vor fi citite amandoua ca foo
, care este un obiect diferit de simbolul FOO
. De aceea cand definesti functii la REPL si el afiseaza numele functiei, o face cu majuscule. Stilul standard, acum, este sa scrii cod cu litere mici si sa lasi reader-ul sa schimbe numele in majuscule.7Pentru a se asigura ca un nume textual este citit totdeauna ca acelasi simbol, reader-ul internalizeaza simbolurile - dupa ce a citit numele si l-a convertit la majuscule, reader-ul se uita intr-o tabela numita pachet si cauta un simbol existent cu acelasi nume. Daca nu poate gasi unul, il creeaza si il adauga in tabel. Altfel returneaza simbolul din tabel. De aceea, de fiecare data cand e intalnit un acelasi nume in expresiile S, el va fi reprezentat cu acelasi obiect.8
Deoarece numele pot contine mult mai multe caractere in Lisp decat in limbajele derivate din Algol, exista cateva conventii de numire specifice Lisp, cum ar fi nume cu cratima, in stilul
hello-world
. Alta conventie importanta este ca variabilele globale primesc nume care incep si se termina cu *
. Similar, numele constantelor incep si se termina cu +
. Si unii programatori denumesc anumite functii de nivel jos cu nume care incep cu %
sau chiar %%
. Numele definite in standardul limbajului folosesc doar caracterele alfabetului latin (A-Z) plus *
, +
, -
, /
, 1
, 2
, <
, =
, >
si &
.Sintaxa pentru liste, numere, siruri de caractere si simboluri poate descrie un procent destul de mare de programe Lisp. Exista si alte reguli, pentru notatia vectorilor, a caracterelor individuale si a array-urilor, pe care le voie acoperi cand voi discuta despre tipurile de date respective in Capitolele 10 si 11. Deocamdata trebuie inteles cum se pot combina numerele, string-urile si simbolurile cu liste delimitate de paranteze pentru a construi expresii S reprezentand arbori de obiecte. Cateva exemple simple:
x ; simbolul X () ; lista vida (1 2 3) ; o lista cu trei numere ("foo" "bar") ; o lista cu doua string-uri (x y z) ; o lista cu trei simboluri (x 1 "foo") ; o lista cu un simbol, un numar si un string (+ (* 2 3) 4) ; o lista cu un simbol, o lista si un numar.Un exemplu doar putin mai complex este lista urmatoare din patru elemente, continand doua simboluri, lista vida si inca o lista, ultima continand doua simboluri si un string:
(defun hello-world () (format t "hello, world"))
Expresii S Ca Forme Lisp
Dupa ce reader-ul a tradus o bucata de text in expresii S, acestea pot fi evaluate drept cod Lisp. Sau macar unele dintre ele pot - nu orice expresie S pe care o poate citi reader-ul poate fi evaluata ca si cod Lisp. Regula de evaluare a Common Lisp defineste un al doilea nivel de sintaxa care determina care expresii S pot fi tratate ca forme Lisp.9 Regulile sintactice la acest nivel sunt destul de simple. Orice atom - orice non-lista sau lista vida - este o forma Lisp legala la fel cum e orice lista care are un simbol drept prim element.10
Desigur, lucrul interesant despre formele Lisp nu este sintaxa lor ci modul de evaluare. De dragul discutiei, te poti gandi la evaluator ca la o functiecare primeste ca argument o forma Lisp corect formata dpdv sintactic si care returneaza o valoare, pe care o putem numi valoarea formei. Desigur, cand evaluatorul este compilator, asta e o viziune simplificata - in acest caz, evaluatorul primeste o expresie si genereaza cod care va calcula valoarea potrivita la momentul rularii. Dar aceasta simplificare ma lasa sa descriu semantica Common Lisp pe baza diferentelor de evaluare a formelor Lisp de catre aceasta functie notionala.
Cele mai simple forme Lisp, atomii, pot fi impartiti in doua categorii: simbolurile si restul. Un simbol, evaluat ca forma, este considerat a fi numele unei variabile si evalueaza ca valoarea curenta a variabilei.11 Voi discuta in Capitolul 6 despre cum isi primesc variabilele valorile la inceput. De asemenea, trebuie observat ca anumite "variabile" sunt un vechi oximoron in programare: "variabile constante". De exemplu, simbolul
PI
numeste o variabila constanta a carei valoare este cea mai buna aproximare posibila a constantei matematice pi.Toti ceilalti atomi - dintre care pana acum ai intalnit numere si siruri de caractere - sunt obiecte auto-evaluatoare. Asta inseamna ca atunci cand o asemenea expresie e transmisa functiei de evaluare notionala, este pur si simplu returnata. Ai vazut exemple de obiecte auto-evaluatoare in Capitolul 2 cand ai scris
10
si "hello, world"
in REPL.De asemenea e posibil ca simbolurile sa fie auto-evaluatoare in sensul ca variabilelor pe care le numesc li se poate atribui valoarea simbolului insusi. Doua constante importante definite astfel sunt
T
si NIL
, valorile canonice pentru adevarat si fals. Voi discuta rolul lor ca booleene in sectiunea "Adevar, Falsitate si Egalitate".Alta clasa de simboluri auto-evaluatoare sunt simbolurile cuvinte cheie - simboluri ale caror nume incep cu
:
. Cand reader-ul internalizeaza un astfel de nume, defineste automat si o variabila constanta cu numele respectiv avand simbolul ca valoare.Lucrurile devin mai interesante cand ne gandim cum sunt evaluate listele. Toate formele-lista legale incep cu un simbol, dar trei tipuri de forme-lista sunt evaluate in trei moduri diferite. Pentru a determinat ce tip de forma este o lista data, evaluatorul trebuie sa determine daca simbolul care incepe lista este numele unei functii, al unui macro sau un operator special. Daca simbolul nu a fost definit inca - cum se intampla daca vrei sa compilezi cod care contine referinte la functii inca nedefinite - se presupune ca e nume de functie.12 Ma voi referi la cele trei tipuri de forme drept forme apeluri de functie, forme macro-uri si forme speciale.
Apeluri de Functii
Regula de evaluare pentru formele apeluri de functii este simpla: evalueaza elementele care raman in lista ca forme Lisp si transmite valorile rezultate functiei numite de primul element. Regula presupune cateva constrangeri sintactice in plus pentru o forma apel de functie: toate elementele unei liste dupa primul element trebuie sa fie ele insele forme Lisp corecte. Cu alte cuvinte, sintaxa de baza a unui apel de functie este in felul urmator, fiecare argument fiind la randul lui o forma Lisp:
(nume-functie argument*)Deci, in urmatoarea expresie intai este evaluat
1
, apoi 2
si apoi se transmit valorile rezultate functiei +
, care returneaza 3:(+ 1 2)O expresie mai complexa, cum ar fi urmatoarea, este evaluata intr-un mod asemanator doar ca evaluarea argumentelor
(+ 1 2)
si (- 3 4)
presupune intai evaluarea argumentelor lor si transmiterea lor catre functia potrivita:(* (+ 1 2) (- 3 4))In cele din urma, valorile 3 si -1 sunt transmise functiei
*
, care returneaza -3.Dupa cum se poate vedea din aceste exemple, functiile sunt folosite pentru multe din lucrurile care in alte limbaje necesita sintaxa speciala, ceea ce pastreaza sintaxa Lisp uniforma.
Operatori Speciali
Acestea fiind zise, nu toate operatiunile pot fi definite ca functii. Deoarece toate argumentele unei functii sunt evaluate inainte de apelul functiei, nu se poate scrie o functie care sa se comporte la fel cu operatorul
IF
folosit in Capitolul 3. Pentru a vedea de ce, sa luam aceasta forma:(if x (format t "yes") (format t "no"))Daca
IF
era functie, evaluatorul ar evalua expresiile argument de la stanga la dreapta. Simbolul x
ar fi evaluat ca o variabila care ar returna o valoare; apoi (format t "yes")
ar fi evaluat ca apel de functie, returnand NIL
dupa ce ar afisa "yes" la iesirea standard. Apoi (format t "no")
ar fi evaluat, afisand "no" si returnand de asemenea NIL
. Numai dupa evaluarea celor trei ar fi transmise valorile rezultate catre IF
, prea tarziu pentru ca acesta sa mai poata controla care din expresiile FORMAT
este evaluata.Pentru a rezolva aceasta problema, Common Lisp defineste cativa asa-numiti operatori speciali care fac lucruri pe care functiile nu le pot face, iar
IF
este unul dintre ei. Sunt 25 in total, dar numai cativa sunt folositi direct in programarea de zi cu zi.13Cand primul element dintr-o lista e un simbol care denumeste un operator special, restul expresiilor sunt evaluate conform regulilor acelui operator.
Regula pentru
IF
e destul de simpla: evalueaza intai prima expresie. Daca rezulta non-NIL
, atunci evalueaza expresia urmatoare si intoarce valoarea respectiva. Altfel intoarce valoarea rezultata din evaluarea celei de treia expresii sau NIL
daca aceasta este omisa. Cu alte cuvinte, forma de baza a unei expresii IF
este astfel:(if test-form then-form [ else-form ])test-form va fi evaluata intotdeauna si apoi urmeaza una din then-form sau else-form.
Un operator special chiar mai simplu de atat este
QUOTE
, care primeste o singura expresie ca "argument" si o returneaza neevaluata. De exemplu, expresia urmatoare este evaluata ca lista (+ 1 2)
, nu ca valoarea 3:(quote (+ 1 2))Nu e nimic special in lista asta; poate fi manipulata la fel cu oricare alta lista creata cu functia
LIST
.14QUOTE
este folosit suficient de des incat sa existe o sintaxa speciala construita pentru el in reader. In loc sa scrii: (quote (+ 1 2))poti sa scrii:
'(+ 1 2)Aceasta sintaxa este o mica extensie a sintaxei de expresii S inteleasa de reader. Din punctul de vedere al evaluatorului, amandoua expresiile arata la fel: o lista al carei prim element este simbolul
QUOTE
si al carei cel de-al doilea element este lista (+ 1 2)
.15In general, operatorii speciali implementeaza capabilitati ale limbajului care necesita procesare speciala din partea evaluatorului. De exemplu, cativa operatori speciali manipuleaza mediul in care vor fi evaluate alte forme. Unul din acestia, despre care voi discuta mai detaliat in Capitolul 6, este
LET
, care este folosit pentru a crea legaturi noi de variabile. Urmatoarea forma este evaluata ca 10 deoarece al doilea x
este evaluat intr-un mediu in care este numele unei variabile careia i-a fost atribuita valoarea 10 de catre LET
: (let ((x 10)) x)
Macro-uri
In timp ce operatorii speciali extind sintaxa Lisp peste ceea ce poate fi exprimat cu apelurile de functii, setul de operatori speciali este fixat de standardul limbajului. Macro-urile, pe de alta parte, dau utilizatorilor limbajului o modalitate de a-i extinde sintaxa. Dupa cum ai vazut in Capitolul 3, un macro este o functie care ia expresii S ca argumente si returneaza o forma Lisp care este evaluata in locul formei macro-ului. Evaluarea unei forme macro are loc in doua faze: mai intai elementele formei macro sunt transmise, neevaluate, functiei macro. Apoi, forma returnata de functia macro - denumita expansiune - este evaluata conform regulilor normale de evaluare.
Este important sa pastrezi clar delimitate in minte cele doua faze ale evaluarii unei forme macro. Este usor sa pierzi sirul cand scrii expresii la REPL deoarece cele doua faze au loc una dupa cealalta si valoarea celei de-a doua faze este returnata imediat. Dar cand codul Lisp e compilat, cele doua faze au loc la momente diferite, de aceea este important sa stii clar ce si cand se intampla. De exemplu, cand compilezi un fisier intreg de cod sursa cu functia
COMPILE-FILE
, toate formele macro din fisier sunt expandate recursiv pana cand codul este alcatuit din nimic altcea decat forme apeluri de functie si forme speciale. Acest cod fara macro-uri este apoi compilat intr-un fisier FASL pe care functia LOAD
stie cum sa-l incarce. Codul compilat nu este executat pana cand fisierul nu este incarcat. Deoarece macro-urile isi genereaza expansiunea la momentul compilarii, ele pot sa faca destul de multa treaba in generarea expansiunii fara sa mai fie nevoie de asta cand fisierul este incarcat sau cand sunt apelate functiile definite in el.Deoarece evaluatorul nu evalueaza elementele formei macro inainte de a le transmite functiei macro, acestea nu trebuie sa fie forme Lisp corect-formate. Fiecare macro atribuie un inteles expresiilor S din forma macro bazat pe modul in care le foloseste pentru a-si genera expansiunea. Cu alte cuvinte, fiecare macro isi defineste o sintaxa proprie. De exemplu, macro-ul
backwards
din Capitolul 3 defineste o sintaxa in care o expresie legala backwards
daca este o lista care este reversul unei forme legale Lisp.Voi vorbi mai mult despre macro-uri pe parcursul cartii. Deocamdata lucrul important de retinut este ca macro-urile, desi sintactic similare apelurilor de functii, sunt folositoare in alt fel, punand la dispozitie o intrare in compilator.16
Adevar, Falsitate si Egalitate
Trebuie sa mai cunosti inca doua lucruri de baza: notiunea pe care o are Common Lisp despre adevar si egalitate si ce inseamna cand doua obiecte Lisp sunt "egale" ("equal"). Adevarul si falsitatea sunt - in acest domeniu - simple: simbolul
NIL
este singura valoare falsa si orice altceva este adevarat. Simbolul T
este valoarea canonica de adevar si poate fi folosit atunci cand ai nevoie sa intorci o valoare non-NIL
si nu ai altceva la indemana. Singurul lucru mai special de tinut minte in legatura cu NIL
este ca e singurul obiect care este si atom si lista in acelasi timp: pe langa "fals" este folosit si pentru a reprezenta lista vida.17 Echivalenta aceasta dintre NIL
si lista vida este construita in reader: daca acesta citeste ()
, il citeste ca simbolul NIL
. Sunt complet interschimbabile. Si deoarece NIL
, dupa cum am mentionat anterior, este numele unei variabile constante cu simbolul NIL
ca valoare, expresiile nil
, ()
, 'nil
si '()
toate sunt evaluate ca acelasi lucru - formele necitate sunt evaluate ca referinta la variabila constanta a carei valoare este simbolul NIL
, iar in cele citate operatorul special QUOTE
evalueaza direct ca simbol. Din acelasi motiv, atat t
cat si 't
se evalueaza ca acelasi lucru: simbolul T
.Folosirea frazelor gen "acelasi lucru" duce la intrebarea: ce inseamna ca doua valori sunt "acelasi lucru". Dupa cum vei vedea in capitolele viitoare, Common Lisp pune la dispozitie un numar de predicate de egalitate specific tipurilor de date:
=
este folosit pentru compararea numerelor, CHAR=
pentru caractere si tot asa. In aceasta sectiune voi discuta cele patru predicate de egalitate "generice" - functii carora li se transmit oricare doua obiecte Lisp si care returneaza adevarat daca sunt echivalente si fals altfel. Ele sunt, in ordinea discriminarii, EQ
, EQL
, EQUAL
si EQUALP
.EQ
testeaza "identitatea obiectelor" - doua obiecte sunt EQ
daca sunt identice. Din pacate, identitatea obiectelor in cazul numerelor si caracterelor depinde de modul in care aceste tipuri de date sunt implementate in fiecare Lisp. De aceea, EQ
poate sa considere doua numere sau doua caractere cu aceeasi valoare ca fiind echivalente, sau nu. Implementarile au suficienta libertate incat expresia (eq 3 3)
poate sa fie evaluata in mod legal ca adevarat sau fals. Mai la obiect, (eq x x)
poate fi evaluat ca adevarat sau fals daca valoarea lui x
se intampla sa fie un numar sau caracter.Deci, nu ar trebui sa folosesti niciodata
EQ
pentru a compara valori care pot fi numere sau caractere. S-ar putea sa ai impresia ca functioneaza predictibil pentru anumite valori intr-o anumita implementare, dar nu ai nici o garantie ca acest lucru se va pastra cand schimbi implementarea. Si asta poate sa insemne chiar si actualizarea, descarcarea unei noi versiuni - daca implementatorul Lisp respectiv schimba modul de reprezentare al numerelor sau caracterelor, comportamentul lui EQ
s-ar putea schimba la randul lui. De aceea Common Lisp defineste
EQL
care se comporta la fel cu EQ
dar este garantat sa considere echivalente doua obiecte de aceeasi clasa reprezentand aceeasi valoare numar sau caracter. Deci, (eql 1 1)
e garantat sa fie adevarat. Si (eql 1
1.0)
e garantat sa fie fals deoarece valoarea intreaga 1 si valoarea in virgula mobila 1.0 sunt instante ale unor clase diferite.Exista doua moduri (scoli) de gandire referitor la folosirea predicatelor
EQ
si EQL
: Tabara "foloseste EQ
oricand posibil" spune ca ar trebui folosit EQ
cand stii ca nu vei compara numere sau caractere deoarece (a) e un mod de a indica faptul ca nu vei compara numere sau caractere si (b) va fi ceva mai eficient deoarece EQ
nu trebuie sa verifice daca argumentele sunt numere sau caractere.Tabara "foloseste totdeauna
EQL
" spune ca nu ar trebui sa folosesti EQ
deoarece (a) castigul potential in claritate este pierdut pentru ca de fiecare data cand cineva care iti citeste codul - inclusiv tu - vede un EQ
, trebuie sa se opreasca si sa verifice daca este folosit corect (cu alte cuvinte ca nu va fi folosit pentru compararea numerelor sau caracterelor) si (b) diferenta de eficienta intre EQ
si EQL
este doar un mic zgomot prin comparatie cu adevaratele gatuiri de performanta. Codul din aceasta carte este scris in stil "foloseste totdeauna
EQL
".18Celelalte doua predicate de egalitate,
EQUAL
si EQUALP
, sunt generale in sensul ca pot opera pe orice tipuri de obiecte, dar sunt mult mai putin fundamentale decat EQ
sau EQL
. Fiecare defineste o notiune de echivalenta din ce in ce mai putin discriminatoare fata de EQL
, permitand diferitelor obiecte sa fie considerate echivalente. Nu exista nimic special in legatura cu notiunile de echivalenta pe care le implementeaza aceste functii cu exceptia faptului ca au fost gasite utile de programatorii Lisp din trecut. Daca aceste predicate nu-ti sunt utile, iti poti defini oricand propria functie predicat care compara diferite tipuri de obiecte cum ai tu nevoie.EQUAL
slabeste discriminarea lui EQL
si considera listele ca fiind echivalente daca au aceleasi structura si continut, recursiv, conform cu EQUAL
. EQUAL
considera sirurile de caractere echivalente daca sunt formate din aceleasi caractere. El este mai permisiv decat EQL
si la vectorii de biti si la obiectele de tip cai in sistemul de fisiere (pathname), doua tipuri de date despre care voi vorbi in capitole viitoare. Pentru toate celelalte tipuri se bazeaza pe EQL
.EQUALP
este similar cu EQUAL
atat doar ca este si mai putin discriminant. Doua string-uri sunt considerate echivalente cand contin aceleasi caractere, ignorand diferentele date de folosirea majusculelor. Considera si doua caractere ca fiind echivalente daca unul e majuscul iar celalalt nu. Numerele sunt echivalente in EQUALP
daca reprezinta aceeasi valoare matematica. Deci, (equalp 1 1.0)
e adevarat. Listele cu elemente EQUALP
sunt EQUALP
; la fel, sirurile (arrays) cu elemente EQUALP
sunt EQUALP
. La fel ca la EQUAL
, exista cateva tipuri de date pe care nu le-am acoperit inca pentru care EQUALP
poate considera doua obiecte ca fiind echivalente, desi nici EQL
nici EQUAL
n-ar face-o. Pentru toate celelalte tipuri de date, EQUALP
se bazeaza pe EQL
. Formatarea Codului Lisp
Desi formatarea nu este, vorbind strict, nici problema de sintactica si nici de semantica, formatarea corecta e importanta pentru citirea si scrierea fluenta si idiomatica a codului. Cheia formatarii codului Lisp este sa-l paragrafezi (indentezi) cum se cuvine. Paragrafarea ar trebui sa reflecte structura codului astfel incat sa nu trebuiasca sa numeri parantezele pentru a vedea ce se intampla. In general, fiecare nou nivel de imbricare e retras mai mult si, daca sunt necesare linii noi, elementele din acelasi nivel de imbricare sunt aliniate. Asadar, un apel de functie care trebuie despartit pe mai multe linii ar putea fi scris astfel:
(some-function arg-with-a-long-name another-arg-with-an-even-longer-name)Macro-urile si formele speciale care implementeaza constructii de control sunt paragrafate diferit de obicei: elementele "body" sunt puse la o distanta de doua spatii relativ la paranteza de deschidere a formei. Deci:
(defun print-list (list) (dolist (i list) (format t "item: ~a~%" i)))Dar nu trebuie sa iti faci prea multe griji in legatura cu regulile astea deoarece un mediu Lisp bun, cum e SLIME, se va ocupa de ele in locul tau. De fapt, unul din avantajele sintaxei Lisp este ca e destul de usor de paragrafat de catre editoarele de text. Deoarece paragrafarea ar trebui sa reflece structura codului iar aceasta e marcata de paranteze, e usor sa lasi editorul sa paragrafeze pentru tine.
In SLIME, apasarea lui Tab la inceputul fiecarei linii va face ca aceasta sa fie paragrafata corespunzator, sau poti reindenta o expresie intreaga pozitionand cursorul pe paranteza de deschidere si tastand
C-M-q
. Sau poti re-paragrafa tot corpul unei forme-functie oriunde din interiorul sau tastand C-c M-q
.Programatorii Lisp experimentati tind sa se bazeze pe editorul lor ca se ocupa de paragrafare automat, nu doar pentru a face codul sa arate bine ci si pentru detectarea greselilor de tastare: odata ce te-ai obisnuit cu modul de paragrafare a codului, o paranteza pusa gresit va fi imediat recunoscuta de paragrafarea ciudata data de editor. De exemplu, sa zicem ca scrii o functie care arata cam asa:
(defun foo () (if (test) (do-one-thing) (do-another-thing)))Acum sa zicem ca din greseala n-ai pus paranteza de inchidere dupa
test
. Din cauza ca nu numeri parantezele, cel mai probabil adaugi o paranteza in plus la sfarsitul formei DEFUN
, rezultand:(defun foo () (if (test (do-one-thing) (do-another-thing))))Dar, daca ai fi paragrafat cu Tab la inceputul fiecarei linii, nu ai fi avut codul asta, ci:
(defun foo () (if (test (do-one-thing) (do-another-thing))))Cand vezi clauzele then si else puse in felul asta sub conditie in loc sa fie doar un pic mai retrase decat
IF
iti arata imediat ca ceva nu e in regula.Alta regula importanta de formatare este ca paranteza de inchidere este pusa totdeauna pe aceeasi linie cu ultimul element din lista pe care o inchide. Adica, nu scrie asa:
(defun foo () (dotimes (i 10) (format t "~d. hello~%" i) ) )ci asa:
(defun foo () (dotimes (i 10) (format t "~d. hello~%" i)))Sirul de
)))
de la sfarsit poate sa para coplesitor, dar atata timp cat codul e paragrafat cum trebuie parantezele ar trebui sa dispara - nu trebuie sa le dai mai multa importanta raspandindu-le pe mai multe linii.In sfarsit, comentariile ar trebui prefatate cu unul pana la patru ";" in functie de locul comentariului, dupa cum urmeaza:
;;;; Patru punct-si-virgule sunt folosite pentru comentarii in antetul fisierului. ;;; Un comentariu cu trei punct-si-virgule va fi de obicei un comentariu de paragraf ;;; care se aplica unei sectiuni mari de cod care urmeaza (defun foo (x) (dotimes (i x) ;; Doua punct-si-virgule indica aplicarea comentariului unei bucati de cod ;; care urmeaza. De observat ca acest comentariu este la acelasi nivel ;; cu codul care ii urmeaza (some-function-call) (another i) ; acest comentariu se aplica numai la linia curenta (and-another) ; alt comentariu de o linie (baz)))Acum esti gata sa incepi sa te uiti mai detaliat la blocurile de cod Lisp, la functii, variabile si macro-uri. Urmeaza: functii.
1
2Implementatorii Lisp, la fel cu implementatorii oricarui alt limbaj, au la dispozitie mai multe moduri in care pot implementa un evaluator, de la interpretor "pur" care interpreteaza obiectele date evaluatorului direct catre un compilator care traduce obiectele in cod masina pe care apoi il ruleaza. La mijloc sunt implementarile care compileaza codul sursa intr-o forma intermediara cum ar fi cod pentru o masina virtuala si apoi interpreteaza codul respectiv. Cele mai multe implementari Common Lisp din zilele noastre folosesc o forma de compilare chiar si cand evalueaza codul la momentul executiei.
3Uneori sintagma expresie S se refera la reprezentarea textuala si uneori la obiectele care rezulta din citirea reprezentarii textuale. De obicei ori reiese din context sensul ei ori distinctia nu conteaza atat de mult.
4Nu toate obiectele Lisp pot fi scrise intr-un mod in care sa poata fi citite ulterior. Dar orice lucru care poate fi citit cu
5Lista vida,
6De fapt, dupa cum vei vedea mai tarziu, numele nu sunt legate intrinsec de un anume tip de obiect. Poti folosi acelasi nume, in functie de context, pentru a te referi atat la o variabila cat si la o functie, pe langa alte cateva posibilitati.
7Faptul ca reader-ul converteste la majuscule poate fi configurat, dar intelegerea timpului si locului in care trebuie schimbat cere o discutie mult mai profunda despre relatiile dintre nume, simboluri si alte elemente de program, si inca nu este momentul potrivit pentru aceasta.
8Voi discuta despre relatia dintre simboluri si pachete mai detaliat in Capitolul 21.
9Desigur, exista si alte nivele de corectitudine in Lisp, la fel ca in alte limbaje. De exemplu, expresia S care rezulta din citirea
10Un alt tip mai putin folosit de forma Lisp este o lista al carei prim element este o forma lambda. Voi discuta despre acest tip de forma in Capitolul 5.
11Exista inca o posibilitate - e posibil sa definesti macro-uri simbol care sunt evaluate usor diferit. Nu ne vom ocupa de ele.
12In Common Lisp un simbol poate sa numeasca atat un operator - functie, macro sau operator special - cat si o variabila. Aceasta este una din diferentele majore dintre Common Lisp si Scheme. Diferenta este descrisa uneori numind Common Lisp un Lisp-2 iar Scheme un Lisp-1 - un Lisp-2 are doua domenii de definitie (namespace), unul pentru operatori si unul pentru variabile, iar un Lisp-1 are unul singur. Amandoua alegerile au avantaje, si partizantii pot dezbate la nesfarsit despre care e mai bun.
13Celelalte au capabilitati utile, dar oarecum ezoterice. Voi discuta despre ele pe masura ce capabilitatile pe care le folosesc intra in discutie.
14Ei bine, tot exista o diferenta - obiectele literale cum ar fi listele citate, dar incluzand sirurile de caractere intre ghilimele, sirurile literale (literal arrays) si vectorii (a caror sintaxa o vei vedea mai tarziu) nu trebuie modificate. In consecinta, orice lista pe care ai de gand sa o modifici ulterior ar trebui creata cu
15Sintaxa aceasta este un exemplu de macro pentru reader. Acestea modifica sintaxa pe care o foloseste reader-ul pentru a traduce text in obiecte Lisp. Este chiar posibil sa iti definesti propriile macro-uri pentru reader, dar este o facilitate putin folosita a limbajului. Cand majoritatea programatorilor Lisp vorbesc despre "extinderea sintaxei" limbajului, vorbesc despre macro-uri obisnuite, dupa cum vom vedea in curand.
16Cei fara experienta in folosirea macro-urilor Lisp sau, mai rau, purtand inca cicatricile ranilor pricinuite de preprocesorul C tind sa devina nervosi cand isi dau seama ca apelurile la macro-uri arata la fel ca apelurile la functii. Asta nu e o problema in practica din mai multe motive. Unul este ca formele macro sunt formatate diferit de apelurile de functii, de obicei. De exemplu, scrii:
Si chiar daca o forma
17Folosirea listei vide ca fals este o reflectare a mostenirii lui Lisp ca limbaj de procesare a listelor la fel cum folosirea numarului intreg 0 ca fals in C reflecta mosternirea sa ca limbaj de lucru la nivel de bit. O alta diferenta subtila din multe pe baza careia un razboi Common Lis vs. Scheme poate sa dureze zile intregi este din cauza valorii distincte pentru fals din Scheme,
18Chiar si standardul limbajului este ambivalent in legatura cu preferinta pentru
http://www-formal.stanford.edu/jmc/history/lisp/node3.html
2Implementatorii Lisp, la fel cu implementatorii oricarui alt limbaj, au la dispozitie mai multe moduri in care pot implementa un evaluator, de la interpretor "pur" care interpreteaza obiectele date evaluatorului direct catre un compilator care traduce obiectele in cod masina pe care apoi il ruleaza. La mijloc sunt implementarile care compileaza codul sursa intr-o forma intermediara cum ar fi cod pentru o masina virtuala si apoi interpreteaza codul respectiv. Cele mai multe implementari Common Lisp din zilele noastre folosesc o forma de compilare chiar si cand evalueaza codul la momentul executiei.
3Uneori sintagma expresie S se refera la reprezentarea textuala si uneori la obiectele care rezulta din citirea reprezentarii textuale. De obicei ori reiese din context sensul ei ori distinctia nu conteaza atat de mult.
4Nu toate obiectele Lisp pot fi scrise intr-un mod in care sa poata fi citite ulterior. Dar orice lucru care poate fi citit cu
READ
poate fi afisat "lizibil" de catre PRINT
.5Lista vida,
()
, care poate fi scrisa si ca NIL
, este atat atom cat si lista.6De fapt, dupa cum vei vedea mai tarziu, numele nu sunt legate intrinsec de un anume tip de obiect. Poti folosi acelasi nume, in functie de context, pentru a te referi atat la o variabila cat si la o functie, pe langa alte cateva posibilitati.
7Faptul ca reader-ul converteste la majuscule poate fi configurat, dar intelegerea timpului si locului in care trebuie schimbat cere o discutie mult mai profunda despre relatiile dintre nume, simboluri si alte elemente de program, si inca nu este momentul potrivit pentru aceasta.
8Voi discuta despre relatia dintre simboluri si pachete mai detaliat in Capitolul 21.
9Desigur, exista si alte nivele de corectitudine in Lisp, la fel ca in alte limbaje. De exemplu, expresia S care rezulta din citirea
(foo 1 2)
este corect formata sintactic dar poate fi evaluata numai daca foo
este numele unei functii sau al unui macro.10Un alt tip mai putin folosit de forma Lisp este o lista al carei prim element este o forma lambda. Voi discuta despre acest tip de forma in Capitolul 5.
11Exista inca o posibilitate - e posibil sa definesti macro-uri simbol care sunt evaluate usor diferit. Nu ne vom ocupa de ele.
12In Common Lisp un simbol poate sa numeasca atat un operator - functie, macro sau operator special - cat si o variabila. Aceasta este una din diferentele majore dintre Common Lisp si Scheme. Diferenta este descrisa uneori numind Common Lisp un Lisp-2 iar Scheme un Lisp-1 - un Lisp-2 are doua domenii de definitie (namespace), unul pentru operatori si unul pentru variabile, iar un Lisp-1 are unul singur. Amandoua alegerile au avantaje, si partizantii pot dezbate la nesfarsit despre care e mai bun.
13Celelalte au capabilitati utile, dar oarecum ezoterice. Voi discuta despre ele pe masura ce capabilitatile pe care le folosesc intra in discutie.
14Ei bine, tot exista o diferenta - obiectele literale cum ar fi listele citate, dar incluzand sirurile de caractere intre ghilimele, sirurile literale (literal arrays) si vectorii (a caror sintaxa o vei vedea mai tarziu) nu trebuie modificate. In consecinta, orice lista pe care ai de gand sa o modifici ulterior ar trebui creata cu
LIST
.15Sintaxa aceasta este un exemplu de macro pentru reader. Acestea modifica sintaxa pe care o foloseste reader-ul pentru a traduce text in obiecte Lisp. Este chiar posibil sa iti definesti propriile macro-uri pentru reader, dar este o facilitate putin folosita a limbajului. Cand majoritatea programatorilor Lisp vorbesc despre "extinderea sintaxei" limbajului, vorbesc despre macro-uri obisnuite, dupa cum vom vedea in curand.
16Cei fara experienta in folosirea macro-urilor Lisp sau, mai rau, purtand inca cicatricile ranilor pricinuite de preprocesorul C tind sa devina nervosi cand isi dau seama ca apelurile la macro-uri arata la fel ca apelurile la functii. Asta nu e o problema in practica din mai multe motive. Unul este ca formele macro sunt formatate diferit de apelurile de functii, de obicei. De exemplu, scrii:
(dolist (x foo) (print x))in loc de:
(dolist (x foo) (print x))sau
(dolist (x foo) (print x))cum ai scrie daca
DOLIST
ar fi fost o functie. Un mediu Lisp bun va formata apelurile macro corect automat, chiar si pentru macro-urile definite de utilizator.Si chiar daca o forma
DOLIST
ar fi fost scrisa pe o singura linie, exista mai multe indicii ca este macro: De exemplu, expresia (x foo)
are inteles doar daca x
este numele unei functii sau al unui macro. Combina asta cu aparitia ulterioara a lui x
ca variabila, si e destul de clar ca DOLIST
este un macro care creeaza o legatura pentru o variabila numita x
. Conventiile de denumire ajuta, desigur - constructiile de ciclare, care invariabil sunt macro-uri - primesc frecvent nume incepand cu do.17Folosirea listei vide ca fals este o reflectare a mostenirii lui Lisp ca limbaj de procesare a listelor la fel cum folosirea numarului intreg 0 ca fals in C reflecta mosternirea sa ca limbaj de lucru la nivel de bit. O alta diferenta subtila din multe pe baza careia un razboi Common Lis vs. Scheme poate sa dureze zile intregi este din cauza valorii distincte pentru fals din Scheme,
#f
, care este diferita atat de simbolul nil
cat si de lista vida, care la randul lor sunt diferite intre ele.18Chiar si standardul limbajului este ambivalent in legatura cu preferinta pentru
EQ
sau EQL
. Identitatea obiectelor e definita de EQ
, dar standardul defineste fraza acelasi cand se refera la obiecte pentru a spune EQL
, daca nu cumva este mentionat explicit un alt predicat. Deci, daca vrei sa fii 100% corect dpdv tehnic, poti sa spui ca (- 3 2)
si (- 4 3)
sunt evaluate ca "acelasi" obiect dar nu ca sunt evaluate ca obiecte "identice". Asta este, intr-adevar, o problema mai delicata (angels-on-pinheads ).Sus
Acesta este doar un rezumat, recomand sa citesti capitolul intreg, contine notiuni de baza.
In Common Lisp programele se execute in doi pasi: intai se citesc textele lor de catre cititor (reader) si apoi se evalueaza de catre evaluator. Reader-ul defineste modul in care se transforma sirurile de caractere in expresii S, iar evaluatorul defineste modul in care acestea sunt transformate in forme pe care sa le poata evalua.
Expresiile S sunt liste - delimitate de paranteze si care contin alte expresii S - sau atomi. Atomii sunt orice nu e lista.
Numerele sunt atomi si pot fi scrise in mai multe moduri, de ex: 123, 3/7, 1.0e0 sau -246/2.
Sirurile de caractere se incadreaza intre ghilimele si pot contine orice caractere; chiar si Enter sau ghilimele, daca sunt precedate de "\". "\" provoaca includerea in string a caracterului urmator, indiferent ce ar fi. Exemple: "foo", "fo\"o".
Numele folosite in programele Lisp, ca FORMAT si *db* sunt reprezentate prin obiecte numite simboluri. Reader-ul construieste aceste obiecte din secventele de caractere citite si apoi le introduce intr-un pachet - un fel de tabel care nu admite simboluri duplicat. De fiecare data cand e intalnit un nume in expresiile S el va fi reprezentat cu acelasi obiect - simbol.
Urmeaza ca exemplu o lista care contine, in ordine, doua simboluri, o lista vida si inca o lista, care la randul ei contine doua simboluri si un string:
(defun hello-world () (format t "hello, world"))Expresiile Lisp rezultate dupa citirea codului sursa de catre reader pot fi transmise evaluatorului spre evaluare. Nu orice expresie S este forma Lisp, sa poata fi evaluata.
Atomii pot fi simboluri, in care caz sunt evaluati ca nume de variabile, sau pot fi obiecte auto-evaluatoare, cum ar fi un numar sau un string. Simbolurile cuvinte-cheie sunt tot autoevaluatoare, valoarea lor fiind exact simbolul respectiv; numele lor incepe cu :.
Listele sunt evaluate diferit, in functie de primul element din componenta lor. De regula acesta este numele unei functii, caz in care sunt evaluate elementele ramase in lista si transmise ca parametri intr-un apel catre functia definita de primul simbol.
Daca in schimb primul simbol este operator special, atunci acesta isi defineste propriile reguli de evaluare a elementelor ramase in lista. Operatorii speciali sunt tratati mai pe larg in Capitolul 20.
A treia varianta este ca primul element sa fie numele unui macro. Atunci este cautata functia macro corespunzatoare si i se transmit restul de elemente din lista. Aceasta functie returneaza o alta forma, denumita expansiune, care este evaluata conform regulilor normale de evaluare.
Common Lisp are urmatorii operatori de egalitate, in ordinea strictetii:
- EQ: arata daca doua obiecte sunt identice. Nu se recomanda folosirea lui pentru compararea numerelor sau a caracterelor.
(eq 3 3)
poate sa fie adevarat sau fals, in functie de implementarea concreta a tipurilor de date respective. (eq a a)
- EQL: garanteaza ca doua obiecte de aceeasi clasa care reprezinta aceeasi valoare numar sau caracter sunt egale. Ex:
(eql 1 1)
e garantat adevarat, in timp ce (eql 1 1.0)
e garantat fals, deoarece 1 e numar intreg iar 1.0 e numar in virgula mobila.- EQUAL si EQUALP: vezi mai sus.
(equalp 1 1.0)
este adevarat.De asemenea exista operatori de egalitate specifici tipurilor de date, cum ar fi "=" pentru numere sau "CHAR=" pentru caractere.
Niciun comentariu:
Trimiteți un comentariu
S-ar putea sa nu vedeti comentariul aparand imediat, asta inseamna ca el asteapta aprobarea mea. Aceasta e o masura anti-SPAM.