Copyright © Peter Seibel
Traducere © Razvan Popa
6. Variabile
Urmatorul bloc de baza la care ne vom uita sunt variabilele. Common Lisp suporta doua tipuri de variabile: lexicale si dinamice.1 Aceste doua tipuri corespund in mare parte variabilelor "locale" si "globale" din alte limbaje. Totusi, corespondenta este doar aproximativa. Pe de o parte, variabilele "locale" din unele limbaje seamana mult cu variabilele dinamice din Common Lisp.2 Si pe de alta parte, variabilele locale din unele limbaje sunt definite lexical fara a oferi toate capabilitatile pe care le au variabilele lexicale din Common Lisp. Mai ales, nu toate limbajele care au variabile cu domeniu de vizibilitate lexical suporta inchideri.Pentru a face lucrurile si mai confuze, multe din formele care folosesc variabile pot fi folosite atat cu variabile lexicale cat si dinamice. Deci voi incepe discutand cateva aspecte ale variabilelor din Lisp care se aplica ambelor tipuri si apoi voi acoperi caracteristicile specifice variabilelor lexicale si dinamice. Apoi voi discuta despre operatorul de atribuire cu scop general din Common Lisp,
SETF
, care este folosit pentru a atribui valori noi variabilelor si cam oricarui alt loc in care poate fi pastrata o valoare. Lucruri de Baza Despre Variabile
Ca si in alte limbaje, variabilele din Common Lisp sunt locuri cu nume in care poate sa fie o valoare. Totusi, in Common Lisp, variabilele nu au tip la modul in care il au in limbaje ca Java sau C++. Adica, nu ai nevoie sa declari tipul de obiect pe care-l poate tine fiecare variabila. In loc de asta, o variabila poate sa pastreze valori de orice tip si valorile poarta informatie despre tipul lor, si aceasta informatie poate fi folosita pentru a verifica tipul variabilei la momentul executiei. Deci, Common Lisp este cu tipuri dinamice - erorile de tip sunt detectate dinamic. De exemplu, daca transmiti ceva non-numeric functiei
+
, Common Lisp va semnala eroare de tip. Pe de alta parte, Common Lisp este limbaj cu tipuri puternice de date in sensul ca toate erorile de tip vor fi detectate - nu se poate trata un obiect ca instanta a unei alte clase.3Toate valorile din Common Lisp sunt, cel putin conceptual, referinte la obiecte.4 In consecinta, atribuirea unei noi valori unei variabile schimba ce obiect este referit de variabila dar nu are nici un efect asupra obiectului referit anterior. Dar, daca o variabila are o referinta la un obiect mutabil, poti folosi acea referinta pentru a modifica obiectul si modificarea va fi vizibila in orice parte de cod care are o referinta catre acelasi obiect.
Un mod de a introduce variabile noi pe care deja l-ai folosit este sa definesti parametri de functie. Dupa cum ai vazut in capitolul anterior, cand definesti o functie cu
DEFUN
, lista de parametri defineste ce variabile vor pastra argumentele functiei cand este apelata. De exemplu, aceasta functie defineste trei variabile - x
, y
si z
- pentru a tine argumentele. (defun foo (x y z) (+ x y z))De fiecare data cand o functie este apelata, Lisp creeaza legaturi noi pentru a pastra argumentele transmise de apelantul functiei. O legatura este manifestarea la momentul executiei a unei variabile. O singura variabila - lucrul catre care indici in sursa programului - poate sa aiba multe legaturi diferite in timpul unei executii a programului. O singura variabila poate avea mai multe legaturi chiar si in acelasi timp; parametrii unei functii recursive, de exemplu, sunt legati din nou la fiecare apel la functie.
La fel ca toate variabilele Common Lisp, parametrii functiilor stocheaza referinte la obiecte.5 Deci, poti atribui o valoare noua parametrului unei functii in corpul functiei si acest fapt nu va afecta legaturile create pentru alt apel la aceeasi functie. Dar daca obiectul transmis functiei este mutabil si il modifici in functie, schimbarile vor fi vizibile apelantului deoarece atat apelantul cat si functia apelata vor referi acelasi obiect.
Alta forma care introduce variabile noi este operatorul special
LET
. Scheletul unei forme LET
arata astfel:(let (variabila*) forma*)in care fiecare variabila este o forma de initializare de variabila. Fiecare forma de initializare este ori o lista continand un nume de variabila si o valoare initiala ori - ca scurtatura pentru initializarea unei variabile cu
NIL
- doar un nume de variabila. Urmatoarea forma LET
, de exemplu, leaga cele trei variabile x
, y
si z
cu valorile initiale 10, 20 si NIL
: (let ((x 10) (y 20) z) ...)Cand forma
LET
este evaluata, toate formele de valori initiale sunt evaluate in prima faza. Apoi legaturi noi sunt create si initializate la valorile initiale potrivite inainte sa fie executate formele din corp. In corpul unui LET
, numele variabilelor se refera la legaturile nou-create. Dupa LET
, numele se refera la obiectele referite inainte de LET
, daca refereau ceva. Valoarea ultimei expresii din corp este returnata ca valoarea expresiei
LET
. Ca parametrii functiilor, variabilele introduse cu LET
sunt legate din nou de fiecare data cand Lisp intra in LET
.6 Domeniul de vizibilitate al parametrilor functiilor si variabilelor
LET
- adica zona din program in care numele variabilei poate fi folosit pentru a referi legatura acelei variabile - este delimitat de forma care introduce variabila. Aceasta forma - definitia de functie sau LET
- este numita forma legatoare (WTF???). Dupa cum vei vedea peste putin timp, cele doua tipuri de variabile - lexicale si dinamice - folosesc mecanisme de vizibilitate diferite, dar in ambele cazuri vizibilitatea este delimitata de forma legatoare.Daca incluzi forme legatoare unele in altele si cele interioare introduc variabile cu acelasi nume, legaturile cele mai interne umbresc legaturile externe. De exemplu, cand este apelata urmatoarea functie, se creeaza o legatura pentru parametrul
x
pentru a salva argumentul functiei. Apoi primul LET
creeaza o noua legatura cu valoarea initiala 2, si LET
-ul intern creeaza o alta legatura, de data aceasta cu valoarea initiala 3. Barele din dreapta marcheaza vizibilitatea fiecarei legaturi.(defun foo (x) (format t "Parametru: ~a~%" x) ; |<------ x e argument (let ((x 2)) ; | (format t "LET extern: ~a~%" x) ; | |<---- x e 2 (let ((x 3)) ; | | (format t "LET intern: ~a~%" x)) ; | | |<-- x e 3 (format t "LET extern: ~a~%" x)) ; | | (format t "Parametru: ~a~%" x)) ; |Fiecare referinta la
x
va referi legatura cu cel mai mic domeniu de vizibilitate in care se afla. Odata ce se iese din domeniul unei forme legatoare, intra in vigoare legatura din domeniul de vizibilitate imediat exterior si x
o refera in locul celei anterioare. Deci, apelarea lui foo
duce la urmatorul rezultat: CL-USER> (foo 1) Parametru: 1 LET extern: 2 LET intern: 3 LET extern: 2 Parametru: 1 NILIn capitolele viitoare voi discuta alte constructii care servesc si ca forme legatoare - orice constructie care introduce variabile noi utilizabile doar in acea constructie este o forma legatoare.
De exemplu, in Capitolul 7 vei vedea bucla
DOTIMES
, o bucla de baza cu numarare. Aceasta introduce o variabila care are valoarea unui contor ce este incrementat la fiecare trecere prin bucla. Urmatoarea bucla, de exemplu, care printeaza numerele de la 0 la 9, lega variabila x
: (dotimes (x 10) (format t "~d " x))Alta forma legatoare este o varianta de
LET
, LET*
. Diferenta este ca intr-un LET
, numele de variabile pot fi folosite doar in corp - partea de dupa lista de variabile - dar intr-un LET*
, formele valorilor initiale pentru fiecare variabila pot referi variabile introduse mai devreme in lista de variabile. Deci, se poate scrie astfel: (let* ((x 10) (y (+ x 10))) (list x y))dar nu si astfel:
(let ((x 10) (y (+ x 10))) (list x y))S-ar putea obtine acelasi rezultat cu
LET
-uri imbricate. (let ((x 10)) (let ((y (+ x 10))) (list x y)))
Variabile Lexicale si Inchideri
Implicit, toate formele legatoare din Common Lisp introduc variabile cu domeniu de vizibilitate lexicala. Variabilele de acest tip pot fi referite doar de cod care se afla textual inauntrul formei legatoare. Definirea lexicala ar trebui sa fie familiara oricui a programat in Java, C, Perl sau Python deoarece toate aceste limbaje au variabile "locale" cu domeniu de vizibilitate lexical. Programatorii Algol ar trebui sa se simta ca acasa, deoarece Algol a introdus prima oara vizibilitatea lexicala in anii '60.
Dar, variabilele lexicale din Common Lisp au ceva in plus, cel putin in comparatie cu modelul original Algol. Acel ceva in plus vine din combinatia intre vizibilitatea lexicala si functiile imbricate. Conform regulilor vizibilitatii lexicale, numai codul care se afla textual in forma legatoare poate sa refere o variabila lexicala. Dar ce se intampla cand o functie anonima contine o referinta la o variabila lexicala dintr-un domeniu de definite exterior? De exemplu, in expresia aceasta:
(let ((count 0)) #'(lambda () (setf count (1+ count))))referinta la
count
din forma LAMBDA
ar trebui sa fie legala conform regulilor vizibilitatii lexicale. Si totusi functia anonima care contine referinta va fi returnata ca valoarea a formei LET
si poate fi invocata, via FUNCALL
, de catre cod care nu este in domeniul de vizibilitate a lui LET
. Deci ce se intampla? Dupa cum vedem, atunci cand count
este variabila lexicala, functioneaza pur si simplu. Legatura catre count
creata cand sirul executiei a intrat in forma LET
va ramane valida cat timp e necesar, in acest caz atata vreme cat cineva tine o referinta catre obiectul functie returnat de forma LET
. Functia anonima este numita inchidere deoarece "inchide peste" legatura create de LET
.Lucrul esential de inteles despre inchideri este ca se captureaza legatura, nu valoarea variabilei. Astfel, o inchidere poate sa acceseze nu numai valoarea variabilei peste care inchide ci poate sa ii si atribuie noi valori care vor persista intre apelurile la inchidere. De exemplu, poti captura inchiderea creata cu expresia anterioara intr-o variabila globala astfel:
(defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count)))))Apoi la fiecare invocare valoarea lui count va creste cu unu.
CL-USER> (funcall *fn*) 1 CL-USER> (funcall *fn*) 2 CL-USER> (funcall *fn*) 3O singura inchidere poate sa captureze oricat de multe variabile prin simpla referire la ele. Sau mai multe inchideri pot sa captureze aceeasi legatura. De exemplu, expresia urmatoare returneaza o lista de trei inchideri, una care incrementeaza valoarea legaturii inchise a lui
count
, una care o decrementeaza si una care returneaza valoarea curenta: (let ((count 0)) (list #'(lambda () (incf count)) #'(lambda () (decf count)) #'(lambda () count)))
Variabile Dinamice, a.k.a. Speciale
Legaturile cu vizibilitate lexicala ajuta la pastrarea inteligibilitatii codului limitand vizibilitatea unui nume. De aceea limbajele moderne folosesc definire lexicala pentru variabilele locale. Uneori, totusi, vrei neaparat o variabila globala - o variabila pe care poti sa o referi de oriunde in program. Utilizarea nediscriminatorie a variabilelor globale poate transforma codul intr-o incalceala aproape la fel de mult ca si utilizarea prea libera a
goto
-urilor, totusi variabilele globale au utilizari legitime si exista intr-o forma sau alta in aproape orice limbaj de programare.7 Si dupa cum vei vedea intr-un moment versiunea din Lisp a variabilelor globale, variabilele dinamice sunt si mai utile dar si mai utilizabile.Common Lisp are doua cai pentru crearea variabilelor globale:
DEFVAR
si DEFPARAMETER
. Ambele forme primesc un nume de variabila, o valoare initiala si un sir documentar optional. Dupa ce a fost DEFVAR
-at sau DEFPARAMETER
-at, numele poate fi folosit oriunde pentru a referi legatura curenta a variabilei globale. Dupa cum ai vazut in capitole anterioare, variabilele globale sunt numite in mod conventional cu nume care incep si se termina cu *
. Vei vedea mai tarziu in aceasta sectiune de ce este destul de important sa te conformezi acestei conventii. Exemple de folosire a DEFVAR
si DEFPARAMETER
:(defvar *count* 0 "Numarul de fleacuri construite pana acum.") (defparameter *gap-tolerance* 0.001 "Toleranta permisa in distantele din fleacuri.")Diferenta dintre cele doua forme este ca
DEFPARAMETER
intotdeauna atribuie valoarea initiala variabilei primite in timp ce DEFVAR
o face numai daca variabila e nedefinita. O forma DEFVAR
poate fi folosita si fara valoare initiala pentru a defini o valoare globala fara a-i acorda o valoare. O astfel de variabila se spune ca este nelegata.Practic, ar trebui sa folosesti
DEFVAR
pentru a definit variabile care vor contine date pe care vrei sa le tii chiar si cand faci o schimbare in codul sursa care foloseste variabila. De exemplu, sa zicem ca variabilele definite anterior sunt parte dintr-o aplicatie care controleaza o fabrica de fleacuri. E bine sa definesti variabila *count*
cu DEFVAR
deoarece numarul de fleacuri produse pana acum nu este invalidat doar pentru ca ai facut ceva schimbari in codul de fabricare a fleacurilor.8 Pe de alta parte, se presupune ca variabila
*gap-tolerance*
are efecte asupra comportamentului codului de fabricare. Daca te decizi ca ai nevoie de toleranta mai mica sau mai mare si schimbi valoarea din forma DEFPARAMETER
ai vrea ca schimbarea sa aiba efect cand recompilezi si reincarci fisierul.Dupa ce definesti o variabila cu
DEFVAR
sau DEFPARAMETER
, poti sa o referi de oriunde. De exemplu, ai putea sa definesti aceasta functie pentru a incrementa numarul de fleacuri produse: (defun increment-widget-count () (incf *count*))Avantajul variabilelor globale este ca nu trebuie sa le transmiti in apeluri. Cele mai multe limbaje pastreaza intrarea si iesirea standard ca variabile globale exact din acest motiv - nu stii niciodata cand s-ar putea sa ai nevoie sa afisezi ceva la iesirea standard, si nu vrei ca fiecare functie sa trebuiasca sa accepte si sa transmita mai departe argumente continand aceste fluxuri doar in caz ca cineva mai tarziu ar putea sa aiba nevoie de ele.
Dar, odata ce o valoare, cum e fluxul standard de iesire, e stocata intr-o variabila globala si ai scris cod care refera acea variabila globala, e tentant sa incerci sa modifici comportamentul codului respectiv schimband valoarea variabilei.
De exemplu, sa zicem ca lucrezi la un program care contine niste functii de nivel scazut de jurnalizar (logare) care afiseaza in fluxul din variabila globala
*standard-output*
. Apoi sa zicem ca intr-o parte din program vrei sa capturezi toate datele generate de functiile respective intr-un fisier. Ai putea sa deschizi un fisier si sa atribui fluxul rezultat variabilei *standard-output*
. Acum functiile de nivel scazut vor transmite datele lor in acel fisier.Asta merge pana cand uiti sa setezi
*standard-output*
inapoi la fluxul original cand nu mai ai nevoie de ea. Daca uiti sa resetezi *standard-output*
, celelalte parti din program care folosesc *standard-output*
vor transmite datelor lor tot in fisierul respectiv.9De fapt se pare ca vrei sa ai un mod de a impacheta o bucata de cod in ceva care zice "Tot codul de mai jos - toate functiile apelate de el si toate functiile apelate de acestea si tot asa, pana la functiile de nivelul cel mai scazut - ar trebui sa foloseasca aceasa valoare pentru variabila globala
*standard-output*
." Apoi cand functia de nivel inalt returneaza, valoarea veche a *standard-output*
ar trebui restaurata automat. Chiar asta te lasa sa faci celalalt tip de variabile - variabilele dinamice - din Common Lisp. Cand legi o variabila dinamica - de exemplu intr-un
LET
sau ca parametru de functie - legatura care este creata la intrarea in forma legatoare inlocuieste legatura globala pe durata formei legatoare. Spre deosebire de o legatura lexicala, care poate fi referita doar in domeniul de vizibilitate lexical din interiorul formei legatoare, o legatura dinamica poate fi referita de orice cod invocat in timpul executiei formei legatoare.10 Si de fapt reiese ca toate variabilele globale sunt variabile dinamice.Astfel, daca vrei sa redefinesti temporar
*standard-output*
, o faci relegand-o, sa zicem, cu un LET
. (let ((*standard-output* *some-other-stream*)) (stuff))In orice cod rulat ca rezultat al apelului la
stuff
, referintele la *standard-output*
vor folosi legatura stabilita de LET
. Si cand stuff
returneaza si controlul iese din LET
, noua legatura a *standard-output*
va disparea si referintele ulterioare la *standard-output*
vor vedea legatura care era curenta inainte de LET
. In orice moment, cea mai recent stabilita legatura umbreste toate celelalte legaturi. Conceptual, fiecare legatura noua pentru o variabila dinamica este impinsa intr-o stiva de legaturi pentru variabila respectiva, si refereintele la acea variabila folosesc totdeauna cea mai recenta legatura. Pe masura ce formele legatoare returneaza, legaturile formate de acestea sunt scoase din stiva, expunand legaturile mai vechi.11Un exemplu simplu arata cum functioneaza acest mecanism.
(defvar *x* 10) (defun foo () (format t "X: ~d~%" *x*))
DEFVAR
creeaza o legatura globala pentru variabila *x*
cu valoarea 10. Referinta la *x*
din foo
va cauta dinamic legatura curenta. Daca apelezi foo
de la nivelul superior (top level), legatura globala creata de DEFVAR
este singura disponibila, asa ca afiseaza 10.CL-USER> (foo) X: 10 NILDar poti sa folosesti
LET
pentru a crea o legatura noua care umbreste temporar legatura globala, si foo
va afisa o valoare diferita.CL-USER> (let ((*x* 20)) (foo)) X: 20 NILAcum, daca apelezi
foo
din nou, fara LET
, vede iar legatura globala.CL-USER> (foo) X: 10 NILAcum definim alta functie.
(defun bar () (foo) (let ((*x* 20)) (foo)) (foo))De observat ca apelul din mijloc la
foo
este impachetat intr-un LET
care leaga *x*
de noua valoare 20. Cand rulezi bar
, primesti acest rezultat:CL-USER> (bar) X: 10 X: 20 X: 10 NILDupa cum poti sa vezi, primul apel la
foo
vede legatura globala, cu valoarea 10. Apelul din mijloc, totusi, vede noua legatura, cu valoarea 20. Dar dupa LET
, foo
vede din nou legatura globala.La fel ca la legaturile lexicale, atribuirea unei noi valori afecteaza numai legatura curenta. Pentru a vedea acest lucru, poti redefini
foo
sa includa o atribuire la *x*
.(defun foo () (format t "Inainte de atribuire~21tX: ~d~%" *x*) (setf *x* (+ 1 *x*)) (format t "Dupa atribuire~21tX: ~d~%" *x*))Acum
foo
afiseaza valoarea lui *x*
, o incrementeaza, si o afiseaza din nou. Daca rulezi foo
, vei vedea asta:CL-USER> (foo) Inainte de atribuire X: 10 Dupa atribuire X: 11 NILNu e vreo surpriza. Acum ruleaza
bar
.CL-USER> (bar) Inainte de atribuire X: 11 Dupa atribuire X: 12 Inainte de atribuire X: 20 Dupa atribuire X: 21 Inainte de atribuire X: 12 Dupa atribuire X: 13 NILVezi ca
*x*
incepe la 11 - apelul anterior la foo
chiar a schimbat valoarea globala. Primul apel la foo
din bar
incrementeaza legatura globala la 12. Apelul din mijloc nu vede legatura globala din cauza lui LET
. Apoi ultimul apel vede legatura globala din nou si o incrementeaza de la 12 la 13. Deci cum functioneaza asta? Cum stie
LET
ca atunci cand leaga *x*
ar trebui sa creeze o legatura dinamica in loc de una lexicala? Stie deoarece numele a fost declarat special.12 Numele oricarei variabile definita cu DEFVAR
sau DEFPARAMETER
este automat declarat global special. Asta inseamna ca oricand folosesti un astfel de nume intr-o forma legatoare - intr-un LET
sau ca parametru de functie sau orice alta constructie care creeaza o legatura noua de variabila - legatura care e creata va fi dinamica. De aceea *conventia*
*de*
*numire*
este atat de importanta - ar fi rau daca ai folosit numele a ceea ce ai crede ca este o variabila lexicala si acea variabila s-ar intampla sa fie globala. Pe de o parte, codul pe care-l apelezi ar putea schimba valoarea legaturii de sub tine; pe de alta, ai putea sa umbresti o legatura stabilita de cod aflat mai sus in stiva. Daca denumesti totdeauna variabilele globale conform cu conventia *
, nu vei folosi o legatura dinamica in locul uneia lexicale niciodata neintentionat.Este de asemenea posibil sa declari un nume special local. Daca, intr-o forma legatoare, declari un nume special, atunci legatura creata pentru acea variabila va fi dinamica, nu lexicala. Alt cod poate declara local un nume ca fiind special pentru a se referi la legatura dinamica. Totusi, variabilele speciale locale sunt destul de rare, deci n-ar trebui sa iti faci prea multe griji in legatura cu ele.13
Legaturile dinamice fac variabilele globale mai usor de manuit, dar este important de observat ca inca permit actiuni de la distanta. Legarea unei variabile globale are doua efecte la distanta - poate schimba comportamentul codului apelat ulterior si deschide si posibilitatea ca acest cod sa atribuie o valoare noua unei legaturi stabilite mai sus in stiva. Variabilele dinamice ar trebui folosite numai cand ai nevoie de una sau amandoua din aceste caracteristici.
Constante
Alt tip de variabile pe care nu l-am mentionat deloc sunt oximoronul "variable constante". Toate constantele sunt globale si sunt definite cu
DEFCONSTANT
. Forma de baza a DEFCONSTANT
este ca la DEFPARAMETER
.(defconstant nume forma-valoare-initiala [ string-documentatie ])La fel ca la
DEFVAR
si DEFPARAMETER
, DEFCONSTANT
are efect global asupra numelui folosit - acesta poate fi folosit ulterior numai pentru referirea la respectiva constanta; nu poate fi folosit drept parametru de functie sau legat in orice alta forma de legare. De aceea, multi programatori Lisp folosesc prin conventie constante cu numele incadrate intre +
. Aceata conventie este oarecum mai putin folosita in mod universal, in comparatie cu cea cu *
folosita la numele speciale globale dar este o idee buna din acelasi motiv.14 Alt lucru de retinut despre
DEFCONSTANT
este ca in timp ce limbajul iti permite sa redefinesti o constanta reevaluand o forma DEFCONSTANT
cu o forma-valoare-initiala diferita, nu este strict definit ce se intampla dupa redefinire. In practica, majoritatea implementarilor iti vor cere sa reevaluezi orice cod care refera acea constanta pentru a vedea valoarea noua, deoarece valoarea veche e foarte posibil sa fie pastrata direct in cod (inline). In consecinta, este o idee buna sa folosesti DEFCONSTANT
numai pentru a defini lucruri despre care stii ca stun intr-adevar constante, cum ar fi valoarea lui NIL. Pentru lucruri pe care s-ar putea sa le schimbi, ar trebui sa folosesti DEFPARAMETER
. Atribuirea
Odata ce ai creat o legatura, poti sa faci doua lucruri cu ea: sa preiei valoarea curenta sau sa ii dai o noua valoare. Dupa cum ai vazut in Capitolul 4, un simbol este evaluat la valoarea variabilei pe care o numeste, deci poti sa preiei noua valoare pur si simplu referind variabila respectiva. Pentru a-i atribui o valoare noua, folosesti macro-ul
SETF
, operatorul de atribuire general din Common Lisp. Forma de baza a lui SETF
este dupa cum urmeaza:(setf loc valoare)Deoarece
SETF
este un macro, el poate sa examineze forma locului in care atribuie si sa se expandeze in operatiunile de nivel jos potrivite pentru manipularea acelui loc. Cand locul este o variabila, el expandeaza intr-un apel catre operatorul special SETQ
, care, fiind operator special, are acces atat la legaturile lexicale cat si la cele dinamice.15 De exemplu, pentru a atribuit valoarea 10 variabilei x
, poti sa scrii astfel:(setf x 10)Dupa cum am discutat mai devreme, atribuirea unei valori noi unei legaturi nu are efect asupra nici unei alte legaturi ale acelei vareiabile. Si nu are nici un efect asupra valorii care era stocata in legatura inainte de atribuire. Deci,
SETF
-ul din aceasta functie: (defun foo (x) (setf x 10))nu va avea efect asupra nici unei valori din afara lui
foo
. Legatura care a fost creata la apelul lui foo
este facuta pe 10, inlocuind imediat orice valoare ar fi fost transmisa ca argument. In particular, o forma cum e urmatoarea:(let ((y 20)) (foo y) (print y))va afisa 20, nu 10, deoarece este valoarea lui
y
transmisa lui foo
unde este pentru putin timp valoarea variabilei x
inainte ca SETF
sa-i dea o noua valoare.SETF
poate sa atribuite si mai multor locuri secvential. De exemplu, in loc de: (setf x 1) (setf y 2)poti sa scrii astfel:
(setf x 1 y 2)
SETF
returneaza valoarea nou atribuita, deci poti sa si inseriezi apelurile catre SETF
ca in expresia urmatoare, care atribuite atat lui x
cat si lui y
aceeasi valoare aleatoare:(setf x (setf y (random 10)))
Atribuiri Generalizate
Legaturile variabilelor nu sunt singurele locuri care pot tine valori, desigur. Common Lisp suporta structuri de date compuse cum ar fi sirurile, tabelele de dispersie si listele, la fel ca si structuri de date definite de utilizator, toate acestea constand in locuri multiple care pot tine o valoare.
Voi acoperi aceste structuri de date in capitole viitoare, dar daca tot suntem la capitolul despre atribuire, ar trebui sa stii ca
SETF
poate atribui o valoare oricarui loc. Pe masura ce acopar diversele tipuri de date compuse, voi arata care functii pot servi ca locuri "SETF
abile." Versiunea pe scurt este ca daca ai nevoie sa atribui o valoare unui loc, SETF
e aproape sigur lucrul pe care sa-l folosesti. E posibil chiar sa-l extinzi pe SETF
pentru a-i permite sa atribuite in locuri definite de utilizator, desi nu voi acoperi aceasta parte.16In aceasta privinta
SETF
nu este diferit de operatorul =
de atribuire din majoritatea limbajelor derivate din C. In acele limbaje, operatorul =
atribuie valori noi variabilelor, elementelor din siruri si campurilor claselor. In limbaje precum Perl si Python care suporta tabelele de dispersie ca tip de data nativ, =
poate sa seteze si valorile intrarilor individuale. Tabela 6-1 sumarizeaza diversele moduri in care =
este folosit in limbajele respective. Tabela 6-1. Atribuirea cu
=
in Alte LimbajeAtribuire catre ... | Java, C, C++ | Perl | Python |
... variabila | x = 10; | $x = 10; | x = 10 |
... element de sir | a[0] = 10; | $a[0] = 10; | a[0] = 10 |
... intrare in tabela de dispersie | -- | $hash{'key'} = 10; | hash['key'] = 10 |
... camp in obiect | o.field = 10; | $o->{'field'} = 10; | o.field = 10 |
SETF
functioneaza la fel -- primul "argument" este un loc in care sa pastrezi valoarea, iar al doilea da valoarea. La fel cu operatorul =
in aceste limbaje, folosesti aceeasi forma pentru exprimarea locului pe care ai folosi-o si pentru a prelua valoarea din el.17 Deci, echivalentele in Lisp ale atribuirilor din Tabela 6-1 -- stiind ca AREF
este functia de acces in siruri, GETHASH
cauta in tabele de dispersie si camp
ar putea fi o functie care acceseaza un slot numit camp
al unui obiect definit de utilizator -- sunt dupa cum urmeaza: Variabila simpla: (setf x 10) Sir: (setf (aref a 0) 10) Tabela de dispersie: (setf (gethash 'key hash) 10) Slot denumit 'camp': (setf (camp o) 10)De observat ca
SETF
area unui loc care este parte dintr-un obiect mai larg are aceeasi semantica cu SETF
area unei variabile: locul este modificat fara vreun efect asupra obiectului care era stocat inainte acolo. Din nou, acest lucru este similar cu modul in care =
se comporta in Java, Perl si Python.18 Alte Moduri De A Modifica Locuri
In timp ce toate atribuirile pot fi exprimate cu
SETF
, anumite modele care implica atribuirea unei valori noi pe baza valorii curente sunt suficient de comune pentru a justifica existenta propriilor operatori. De exemplu, ai putea sa incrementezi un numar cu SETF
, astfel:(setf x (+ x 1))sau sa-l decrementezi astfel:
(setf x (- x 1))e un pic complicat, pe langa
++x
si --x
din C. In schimb, poti folosi macro-urile INCF
si DECF
, care incrementeaza si decrementeaza un loc cu o anumita valoare, implicit 1.(incf x) === (setf x (+ x 1)) (decf x) === (setf x (- x 1)) (incf x 10) === (setf x (+ x 10))
INCF
si DECF
sunt exemple de un tip de macro numit macro de modificare. Macro-urile de modificare sunt construite peste SETF
si modifica locuri atribuindu-le o valoare noua pe baza valorii curente. Principalul beneficiu al macro-urilor de modificare este ca sunt mai concise decat aceeasi modificare scrisa folosind SETF
. In plus, macro-urile de modificare sunt definite intr-un fel care le face sigure in folosirea cu locuri in care evaluarea locului trebuie facuta o singura data. Un exemplu simplu este expresia urmatoare, care incrementeaza valoarea unui element aleator dintr-un sir: (incf (aref *array* (random (length *array*))))O transpunere naiva intr-o expresie
SETF
ar putea arata astfel:(setf (aref *array* (random (length *array*))) (1+ (aref *array* (random (length *array*)))))Dar nu functioneaza pentru ca cele doua apeluri la
RANDOM
nu returneaza neaparat aceeasi valoare -- expresia mai degraba va prelua valoarea unui element din sir, o va incrementa si apoi o va stoca drept noua valoare a unui alt element. Expresia INCF
, in schimb, functioneaza corect deoarece stie cum sa descompuna aceasta expresie:(aref *array* (random (length *array*)))pentru a extrage partile care ar putea avea efecte secundare pentru a sti sigur ca sunt evaluate o singura data. In acest caz, probabil ca ar expanda in ceva asemanator cu asta:
(let ((tmp (random (length *array*)))) (setf (aref *array* tmp) (1+ (aref *array* tmp))))In general, macro-urile de modificare sunt garantate ca vor evalua atat argumentele cat si subformele formei-loc exact o singura data, de la stanga la dreapta.
Macro-ul
PUSH
, pe care l-ai folosit in mica baza de date pentru a adauga elemente in variabila *db*
, este un alt macro de modificare. Vei vedea mai indeaproape cum functioneaza el si colegii lui POP
si PUSHNEW
in Capitolul 12 cand voi vorbi despre cum sunt reprezentate listele in Lisp.In cele din urma, doua macro-uri usor ezoterice dar utile sunt
ROTATEF
si SHIFTF
. ROTATEF
roteste valorile intre locuri. De exemplu, daca ai doua variabile, a
si b
, acest apel:(rotatef a b)schimba valorile celor doua variabile si returneaza
NIL
. Deoarece a
si b
sunt variabile si nu te intereseaza efectele secundare, expresia ROTATEF
anterioara este echivalenta cu:(let ((tmp a)) (setf a b b tmp) nil)Cu alte tipuri de locuri, expresia echivalenta folosind
SETF
ar fi sensibil mai complexa.SHIFTF
este similar doar ca in loc sa roteasca valorile le translateaza la stanga -- valoarea ultimului argument este mutata in antepenultimul argument, cel din stanga lui si tot asa. Se returneaza valoarea originala a primului argument. Deci: (shiftf a b 10)este echivalent -- din nou, datorita faptului ca nu trebuie sa iti faci griji in legatura cu efectele secundare -- cu asta:
(let ((tmp a)) (setf a b b 10) tmp)Atat
ROTATEF
cat si SHIFTF
pot fi folosite cu orice numar de argumente si, la fel cu toate macro-urile de modificare, sunt garantate sa le evalueze o singura data, in ordine de la stanga la dreapta.Acum ca ai o privire de ansamblu asupra notiunilor de baza despre functiile si variabilele din Common Lisp, esti pregatit sa treci la caracteristica ce inca face diferenta dintre Lisp si alte limbaje: macro-urile.
1Variabilele dinamice sunt numite uneori si variabile speciale din motive pe care le vei vedea mai tarziu in capitol. Este important sa fii constinent de existenta acestui sinonum, deoarece unii oameni (si unele implementari de Lisp) folosesc unul din termeni iar altii il folosesc pe celalalt.
2Lisp-urile vechi aveau tendinta sa foloseasca variabile dinamice pentru variabilele locale, cel putin cand erau interpretate. Elisp, dialectul folosit in Emacs, este oarecum retrograd in acest sens, continuand sa suporte doar variabile dinamice. Alte limbaje au recapitulat aceasta tranzitie de la variabilele dinamice la lexical -- variabilele
3De fapt, nu e chiar adevarat ca toate erorile de scriere vor fi detectate -- e posibil sa se foloseasca declaratii optionale care sa-i spuna compilatorului ca anumite variabile vor contine totdeauna obiecte de un anumit tip si sa se opreasca verificarea la rulare a tipului in anumite portiuni din cod. Totusi, declaratiile de acest tip sunt folosite pentru optimizarea codului dupa ce a fost dezvoltat si depanat, nu in timpul dezvoltarii normale.
4Ca optimizare, anumite tipuri de obiecte, cum ar fi numerele intregi mai mici de un anumit prag si caracterele pot fi reprezentate direct in memorie acolo unde alte obiecte ar fi reprezentate printr-un pointer catre un obiect. Dar, deoarece numerele intregi si caracterele sunt imutabile, nu conteaza ca exista mai multe copii ale "aceluiasi" obiect in diferite variabile. Aceasta este radacina diferencei dintre
5In termeni de scriere a compilatoarelor functiile Common Lisp sunt cu "transmitere-prin-valoare." Totusi, valorile care sunt transmise sunt referinte la obiecte, similar cu modul de lucru din Java si Python.
6Variabilele din formele
8Daca vrei in mod special sa resetezi o variabila definita cu
9Strategia de a atribui temporar *standard-output* face probleme si daca sistemul e multithreaded -- daca sunt mai multe fire de control care incearca sa printeze catre diferite fluxuri in acelasi timp, toate vor incerca sa seteze variabila globala catre fluxul pe care vor sa-l foloseasca si se vor calca in picioare. Ai putea sa folosesti un lock pentru a controla accesul la variabila globala, dar atunci nu prea beneficiezi de fire multiple de executie, deoarece oricare fir ar printa trebuie sa blocheze toate celelalte fire pana cand termina, chiar daca celelalte vor sa printeze catre alt flux.
10Termenul tehnic pentru intervalul in timpul caruia referintele pot fi facute la o legatura este de domeniu de timp. Astfel, domeniul de vizibilitate si domeniu de timp sunt notiuni complementare -- domeniul de vizibilitate se refera la spatiu in timp ce domeniul de timp se refera la timp. Variabilele lexicale au domeniu de vizibilitate lexical dar domeniu de timp nedefinit, adica sunt disponibile pentru un interval nedefinit, in functie de cat este nevoie de ele. Variabilele dinamice, prin contrast, au vizibilitate nedefinita deoarece pot fi referite de oriunde dar au durata de viata dinamica. Pentru a amesteca lucrurile si mai mult, combinatia dintre vizibilitate nedefinita si durata de viata dinamica mai este cunoscuta si ca vizibilitate dinamica (dynamic scope).
11Desi standardul nu specifica modul de incorporare a programarii cu mai multe fire de executie in Common Lisp, implementarile care au aceasta caracteristica urmeaza practica stabilita pe masinile Lisp si creeaza legaturi dinamice per fir. O referinta la o variabila globala va gasi cea mai recenta legatura stabilita in firul curent, sau legatura globala.
12Acesta este motivul pentru care variabilele dinamice mai sunt numite si variabile speciale.
13Daca trebuie sa stii, poti sa cauti
14Cateva constante de baza definite chiar in limbaj nu urmeaza aceasta conventie -- printre care
15Unii programatori Lisp de moda veche prefera sa foloseasca
16Cauta
17Predominanta sintaxei derivate din Algol pentru atribuire cu "locul" pe partea stanga a
lvalue."
18Programatorii in C ar putea sa vrea sa se gandeasca la variabile si alte locuri ca tinand un pointer catre un obiect real; atribuirea unei variabile pur si simplu schimba ce obiect este referit iar atribuirea unei parti dintr-un obiect compus este similara cu indirectarea prin pointerul catre obiectul propriu-zis. Programatorii C++ ar trebui sa observe un lucru: comportamentul lui
2Lisp-urile vechi aveau tendinta sa foloseasca variabile dinamice pentru variabilele locale, cel putin cand erau interpretate. Elisp, dialectul folosit in Emacs, este oarecum retrograd in acest sens, continuand sa suporte doar variabile dinamice. Alte limbaje au recapitulat aceasta tranzitie de la variabilele dinamice la lexical -- variabilele
locale
din Perl, de exemplu, sunt dinamice in timp ce variabilele my
, introduse in Perl 5, sunt lexicale. Python nu a avut niciodata variabile dinamice adevarate dar a introdus vizibilitatea lexicala adevarat abia in versiunea 2.2 (variabilele lexicale din Python inca sunt oarecum limitate prin comparatie cu cele din Lisp datorita combinarii atribuirii si legarii in sintaxa limbajului.)3De fapt, nu e chiar adevarat ca toate erorile de scriere vor fi detectate -- e posibil sa se foloseasca declaratii optionale care sa-i spuna compilatorului ca anumite variabile vor contine totdeauna obiecte de un anumit tip si sa se opreasca verificarea la rulare a tipului in anumite portiuni din cod. Totusi, declaratiile de acest tip sunt folosite pentru optimizarea codului dupa ce a fost dezvoltat si depanat, nu in timpul dezvoltarii normale.
4Ca optimizare, anumite tipuri de obiecte, cum ar fi numerele intregi mai mici de un anumit prag si caracterele pot fi reprezentate direct in memorie acolo unde alte obiecte ar fi reprezentate printr-un pointer catre un obiect. Dar, deoarece numerele intregi si caracterele sunt imutabile, nu conteaza ca exista mai multe copii ale "aceluiasi" obiect in diferite variabile. Aceasta este radacina diferencei dintre
EQ
si EQL
discutata in Capitolul 4.5In termeni de scriere a compilatoarelor functiile Common Lisp sunt cu "transmitere-prin-valoare." Totusi, valorile care sunt transmise sunt referinte la obiecte, similar cu modul de lucru din Java si Python.
6Variabilele din formele
LET
si parametrii functiilor sunt create prin exact acelasi mecanism. De fapt, in unele dialecte de Lisp -- nu Common Lisp -- LET
este doar un macro care expandeaza intr-un apel la o functie anonima. Deci, in acele dialecte, codul urmator:(let ((x 10)) (format t "~a" x))este o forma macro care expandeaza in:
((lambda (x) (format t "~a" x)) 10)7Java deghizeaza variabilele globale ca si campuri statice publice, C foloseste variabile
extern
e, iar variabilele de nivel modul in Python si de nivel pachet in Perl pot fi, la fel, accesate de oriunde.8Daca vrei in mod special sa resetezi o variabila definita cu
DEFVAR
, poti sa o setezi direct cu SETF
sau poti sa o faci nelegata folosind MAKUNBOUND
si apoi sa reevaluezi forma DEFVAR
.9Strategia de a atribui temporar *standard-output* face probleme si daca sistemul e multithreaded -- daca sunt mai multe fire de control care incearca sa printeze catre diferite fluxuri in acelasi timp, toate vor incerca sa seteze variabila globala catre fluxul pe care vor sa-l foloseasca si se vor calca in picioare. Ai putea sa folosesti un lock pentru a controla accesul la variabila globala, dar atunci nu prea beneficiezi de fire multiple de executie, deoarece oricare fir ar printa trebuie sa blocheze toate celelalte fire pana cand termina, chiar daca celelalte vor sa printeze catre alt flux.
10Termenul tehnic pentru intervalul in timpul caruia referintele pot fi facute la o legatura este de domeniu de timp. Astfel, domeniul de vizibilitate si domeniu de timp sunt notiuni complementare -- domeniul de vizibilitate se refera la spatiu in timp ce domeniul de timp se refera la timp. Variabilele lexicale au domeniu de vizibilitate lexical dar domeniu de timp nedefinit, adica sunt disponibile pentru un interval nedefinit, in functie de cat este nevoie de ele. Variabilele dinamice, prin contrast, au vizibilitate nedefinita deoarece pot fi referite de oriunde dar au durata de viata dinamica. Pentru a amesteca lucrurile si mai mult, combinatia dintre vizibilitate nedefinita si durata de viata dinamica mai este cunoscuta si ca vizibilitate dinamica (dynamic scope).
11Desi standardul nu specifica modul de incorporare a programarii cu mai multe fire de executie in Common Lisp, implementarile care au aceasta caracteristica urmeaza practica stabilita pe masinile Lisp si creeaza legaturi dinamice per fir. O referinta la o variabila globala va gasi cea mai recenta legatura stabilita in firul curent, sau legatura globala.
12Acesta este motivul pentru care variabilele dinamice mai sunt numite si variabile speciale.
13Daca trebuie sa stii, poti sa cauti
DECLARE
, SPECIAL
si LOCALLY
in HyperSpec.14Cateva constante de baza definite chiar in limbaj nu urmeaza aceasta conventie -- printre care
T
si NIL
, ceea ce poate sa fie uneori enervant atunci cand cineva vrea sa foloseasca t
drept nume de variabila locala. Alta este PI
, care e cea mai buna aproximare in virgula mobila a constantei matematice pi.15Unii programatori Lisp de moda veche prefera sa foloseasca
SETQ
cu variabilele, dar stilul modern tinde sa foloseasca SETF
pentru toate atribuirile.16Cauta
DEFSETF
, DEFINE-SETF-EXPANDER
pentru mai multe informatii.17Predominanta sintaxei derivate din Algol pentru atribuire cu "locul" pe partea stanga a
=
si valoarea noua pe partea dreapta a dus la aparitia termenului lvalue, prescurtare pentru "valoarea din stanga", insemnand ceva caruia i se poate atribui, si rvalue, insemnand ceva care furnizeaza o valoare. Un hacker de compilatoare ar spune ca "SETF
isi trateaza primul argument calvalue."
18Programatorii in C ar putea sa vrea sa se gandeasca la variabile si alte locuri ca tinand un pointer catre un obiect real; atribuirea unei variabile pur si simplu schimba ce obiect este referit iar atribuirea unei parti dintr-un obiect compus este similara cu indirectarea prin pointerul catre obiectul propriu-zis. Programatorii C++ ar trebui sa observe un lucru: comportamentul lui
=
in C++ cand este vorba de obiecte -- si anume, o copiere a membrilor -- este destul de particular.
Variabilele sunt locuri cu nume in care poate sa fie stocata o valoare.
RăspundețiȘtergereToate valorile in Common Lisp sunt, conceptual cel putin, referinte la obiecte. Prin urmare, atribuind unei variabile o valoare noua se schimba CE obiect este referit de variabila, dar nu se schimba si obiectul referit anterior. Totusi, daca o variabila pastreaza o referinta la un obiect mutabil, se poate folosi referinta pentru a modifica obiectul si modificarea va fi vizibila in orice bucata de cod care are o referinta la acelasi obiect.
Se pot introduce variabile noi:
- definind parametri de functii [1]
- folosind operatorul special LET [2]
Domeniul de definitie - zona de program in care numele de variabila poate fi folosit pentru a referi legatura variabilei - este delimitat de forma care introduce variabila. Forma care introduce legaturi noi se numeste forma legatoare (BINDING FORM). Orice constructie care introduce un nume nou de variabila care este folosibil doar in interiorul acelei constructii este o forma legatoare.
Deoarece formele legatoare pot fi imbricate, la un moment dat se foloseste legatura cea mai interna, care ascunde/umbreste legaturile exterioare
[1]
La fiecare apel de functie Lisp creeaza LEGATURI noi pentru a pastra argumentele transmise de apelantul functiei.
O legatura este manifestarea in timpul rularii a unei variabile.
O variabila - chestia pe care o vezi in surse - poate avea mai multe legaturi diferite in timpul rularii unui program.
Parametrii functiilor stocheaza referinte de obiecte. Poti asigna o valoare noua unui parametru de functie in interiorul functiei, si nu va afecta legaturile create pentru alt apel al aceleiasi functii. Dar daca obiectul transmis unei functii este mutabil si poti il modifici in functie, schimbarile vor fi vizibile apelantului, deoarece atat el cat si apelatul refera acelasi obiect.
[2]
Cand o forma LET este evaluata, toate formele initiale sunt evaluate mai intai. Apoi se creeaza legaturi noi si se initializeaza cu valorile corespunzatoare inainte de a executa formele din corp. In corpul lui LET, numele de variabile se refera la legaturile nou-create. Dupa LET, numele se refera la ce se referea inainte de LET.
Variabilele lexicale sunt variabilele accesibile doar de catre cod aflat textual in cadrul formei legatoare. Variabilele lexicale pot persista o perioada de timp nedefinita daca ele sunt inchise de functii anonime - mai exact, legaturile respective sunt pastrate atata timp cat exista si functiile anonime respective. Asta inseamna ca daca o functie e definita intr-o forma legatoare si refera legaturi din cadrul formei, la iesirea din forma acele legaturi referite de catre functia definita acolo vor fi pastrate, atata timp cat este pastrata si functia respectiva.
RăspundețiȘtergere(setf fu (let ((c 4)) (lambda (x) (+ x c))))
(funcall fu 8) ==> 12
Poti sa definesti o variabila fara ca ea sa aiba valoare.
Variabilele dinamice pot fi accesate de oriunde din program, odata ce au fost definite cu DEFVAR sau DEFPARAMETER. Prima forma din cele doua atribuie valoare doar daca variabila este nedefinita, si poti sa definesti o variabila si fara a-i atribui o valoare.
La variabilele dinamice poti modifica valoarea intr-o forma si apoi cand iesi din forma respectiva variabila revine la valoarea de dinainte. Poti modifica valoarea unei variabile cu macroul SETF sau folosind forme legatoare.
Creezi legaturi cu DEFUN sau LET (sau alte constructii, gen macrouri ca DOTIMES), definite sau nu, si apoi le atribui valori cu macroul SETF, care stie sa expandeze in codul necesar pentru a manipula locul respectiv.
Cand atribui o valoare unei legaturi nu schimbi cu nimic celelalte legaturi ale variabilei respective, si nici valoarea care era stocata in legatura inainte de atribuire.
(defun foo (x) (setf x 10))
(let ((y 20)) (foo y) (print y)) ==> 20
foo nu are efect asupra legaturilor lui x de dinaintea apelarii sale. De aceea, chiar daca (foo y) pare sa modifice pe y, odata iesit din foo, valoarea lui y redevine 20.
In afara de SETF poti folosi alte macrouri modificatoare pentru a modifica obiecte mutabile: INCF, DECF, PUSH, POP, PUSHNEW, ROTATEF, SHIFTF.
Vezi de asemenea o explicatie pentru variabilele speciale/dinamice, in comparatie cu cele lexicale aici: http://www.flownet.com/ron/specials.pdf - "The Idiot's Guide to Special Variables and Lexical Closures", de Erann Gat
RăspundețiȘtergere