Copyright © Peter Seibel
Traducere © Razvan Popa
3. Practic: O Baza de Date Simpla
Evident, inainte sa incepi sa scrii software adevarat in Lisp trebuie sa inveti limbajul. Dar hai sa fim cinstiti - poate te gandesti: "'Practical Common Lisp', oare nu e un oximoron? De ce sa invat un limbaj daca nu stie sa faca ce am eu nevoie?" Asa ca voi incepe dandu-ti un exemple de ce se poate face cu Common Lisp. In acest capitol vei scrie o baza de date simpla pentru urmarirea CD-urilor. Vei folosi tehnici similare in Capitolul 27 cand vei construi o baza de date de MP3-uri pentru server-ul nostru. De fapt, ai putea sa te gandesti la asta ca la o parte din proiectul software pentru MP3-uri - la urma urmei, pentru a avea niste MP3-uri pe care sa le asculti ar fi bine sa stii ce CD-uri ai si care din ele trebuie copiate.In acest capitol, voi acoperi doar atata Lisp cat e necesar ca sa intelegi cum functioneaza codul. Dar voi trece peste destul de multe detalii. Deocamdata nu trebuie sa iti faci probleme in legatura cu chestiile mici - urmatoarele capitole vor acoperi toate constructiile Common Lisp folosite aici, si inca altele, intr-un mod mult mai sistematic.
O observatie legata de terminologie: voi discuta o serie de operatori Lisp in capitolul acesta. In Capitolul 4 vei invata ca existe trei tipuri diferite de operatori in Common Lisp: functii, macro-uri si operatori speciali. Pentru acest capitol nu ai nevoie sa stii care e diferenta, dar ma voi referi la diferitii operatori ca la functii sau macro-uri sau operatori speciali dupa caz, in loc sa incerc sa ascund detaliile in cuvantul
operator. Deocamdata poti trata functie, macro si operator special ca fiind mai mult sau mai putin echivalente.1
De asemenea, tine minte ca nu-ti voi arata cele mai sofisticate tehnici Common Lisp pentru primul program post-"hello, world". Motivatia acestui capitol nu este ca asa se scriu bazele de date in Lisp, ci mai degraba vreau sa vezi cam cum se programeaza in Lisp si cum chiar si un programete destulde simplu poate sa fie destul de capabil.
CD-uri si Inregistrari
Pentru a urmari CD-urile care trebuie copiate ca MP3-uri si care ar trebui copiate mai intai, fiecare inregistrare din baza de date va contine numele albumului, numele artistului, o rating data de utilizator si daca a fost copiat sau nu. Deci vei avea nevoie sa reprezinti intr-un fel o inregistrare individuala - adica un CD. In Common Lisp ai multe variante, de la liste cu patru elemente pana la clase, folosind Sistemul de Obiecte din Common Lisp (Common Lisp Object System - CLOS).
Deocamdata nu ne complicam, vom folosi o lista. Poti sa faci o lista cu functia
LIST
, care, destul de logic, returneaza o lista formata din argumentele primite.CL-USER> (list 1 2 3) (1 2 3)Ai putea sa folosesti o lista de patru elemente, fiecarui element din lista corespunzandu-i un element din inregistrare. Alta varianta de liste, numita lista de proprietati (property list sau plist pe scurt) este si mai buna. Un plist este o lista in care fiecare al doilea element, incepand cu primul, este un symbol care descrie ce fel de element urmeaza in lista. Nu voi explica acum exact ce inseamna un simbol; in principiu e un nume. Pentru simbolurile care denumesc campurile din baza de date de CD-uri, poti sa folosesti un tip particular de simbol, numit simbol cuvant cheie (keyword). Un cuvant cheie este orice nume care incepe cu
:
, de exemplu, :foo
. Un exemplu de plist care foloseste cuvintele cheie:a
, :b
si :c
drept nume de proprietati:CL-USER> (list :a 1 :b 2 :c 3) (:A 1 :B 2 :C 3)Observa ca poti crea o lista de proprietati cu aceeasi functie
LIST
cu care creezi si alte liste; continutul sau o face sa fie lista de proprietati.Plist-urile sunt avantajos de folosit pentru a reprezenta inregistrarile dintr-o baza de date datorita functiei
GETF
, care primeste un plist si un simbol si returneaza valoarea din lista care urmeaza simbolului, facand un plist sa fie ceva gen tabelele de dispersie (hash tables) ale saracului. Lisp are si tabele de dispersie reale, dar plist-urile sunt suficiente in cazul de fata si pot fi salvate mai usor intr-un fisier, ceea ce va fi util mai tarziu.CL-USER> (getf (list :a 1 :b 2 :c 3) :a) 1 CL-USER> (getf (list :a 1 :b 2 :c 3) :c) 3Acestea fiind zise, e usor de scris o functie
make-cd
care sa primeasca cele patru campuri ca argumente si sa returneze un plist reprezentand CD-ul respectiv.(defun make-cd (title artist rating ripped) (list :title title :artist artist :rating rating :ripped ripped))Cuvantul
DEFUN
ne spune ca forma aceasta defineste o functie noua. Numele functiei este make-cd
. Dupa nume urmeaza lista de parametri. Functia are patru parametri: titlu
, artist
, nota
si ripped
. Dupa lista de parametri urmeaza corpul functiei. In cazul de fata este o singura forma, un apel la LIST
. Cand este apelat make-cd
, argumentele transmise in apel vor fi legate in variabilele din lista de parametri. De exemplu, pentru a crea o inregistrare pentru CD-ul Roses de Kathy Mattea, ai putea sa apelezi make-cd
astfel:CL-USER> (make-cd "Roses" "Kathy Mattea" 7 t) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)
Arhivarea CD-urilor
Cu o inregistrare nu se face o baza de date. E nevoie de o constructie mai mare pentru mai multe inregistrari. Din nou, pentru a pastra lucrurile simple, o lista pare sa fie o alegere buna. Tot pentru simplitate, se poate folosi o variabila globala,
*db*
, pe care o poti definit cu macro-ul DEFVAR
. Asteriscurile (*) din nume sunt o conventie de nume din Lisp pentru variabilele grlobale.2(defvar *db* nil)Poti sa folosesti macro-ul
PUSH
pentru a adauga elemente in lista *db*
. Dar probabil ca e o idee buna sa abstractizam lucrurile un pic, deci ar merge definita o functie add-record
care adauga o inregistrare in baza de date.(defun add-record (cd) (push cd *db*))Acum poti folosi
add-record
si make-cd
impreuna pentru a adauga CD-uri in baza de date.CL-USER> (add-record (make-cd "Roses" "Kathy Mattea" 7 t)) ((:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)) CL-USER> (add-record (make-cd "Fly" "Dixie Chicks" 8 t)) ((:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)) CL-USER> (add-record (make-cd "Home" "Dixie Chicks" 9 t)) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))Dupa fiecare apel la
add-record
REPL afiseaza valoarea returnata, care este valoarea continuta in ultima expresie din corpul functiei, PUSH
. Si PUSH
returneaza noua valoare a variabilei pe care o modifica. Deci ceea ce vezi este valoarea bazei de date dupa ce fiecare inregistrare este adaugata.Vizualizarea Continutului Bazei de Date
Poti sa vezi valoarea curenta a lui
*db*
in orice moment tastand *db*
in REPL.CL-USER> *db* ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))Dar nu este un mod foarte satisfacator de a vedea ce contine baza de date. Poti sa scrii o functie
dump-db
care afiseaza continutul bazei de date intr-un format mai lizibil, ceva in genul asta:TITLE: Home ARTIST: Dixie Chicks RATING: 9 RIPPED: T TITLE: Fly ARTIST: Dixie Chicks RATING: 8 RIPPED: T TITLE: Roses ARTIST: Kathy Mattea RATING: 7 RIPPED: TFunctia este definita astfel:
(defun dump-db () (dolist (cd *db*) (format t "~{~a:~10t~a~%~}~%" cd)))Functia cicleaza prin toate elementele din
*db*
cu ajutorul macro-ului DOLIST
, legand fiecare element in variabilacd
. Fiecare valoare a lui cd
este afisata cu FORMAT
. Categoric, apelul la
FORMAT
este un pic criptic. Totusi, FORMAT
nu este mai complicat decat functia printf
din C sau Perl sau operatorul string-%
din Python. In Capitolul 18 voi discuta mai detaliat despre FORMAT
. Deocamdata vom urmari apelul pas cu pas. Dupa cum ai vazut in Capitolul 2, FORMAT
primeste cel putin doua argumente, dintre care primul este fluxul (stream) catre care trimite datele; t
e o scurtatura pentru fluxul standard de iesire (*standard-output*
).Al doilea argument este un sir care specifica formatarea si poate contine atat text literal cat si directive care indica modul de interpolare al restului de argumente. Directivele incep cu
~
(la fel cum directivele printf
incep cu %
). FORMAT
dispune de zeci de directive, fiecare cu propriul set de optiuni.3 Deocamdata ma voi concentra numai pe cele necesare pentru a scrie dump-db
.Directiva
~a
este directiva estetica; ea consuma un argument si il afiseaza intr-un format lizibil, ceea ce pentru cuvintele cheie inseamna fara : din fata si siruri de caractere fara ghilimele. De exemplu:CL-USER> (format t "~a" "Dixie Chicks") Dixie Chicks NILsau:
CL-USER> (format t "~a" :title) TITLE NILDirectiva
~t
este folosita pentru tabulare. ~10t
inseamna ca FORMAT
trebuie sa emita suficiente spatii cat sa ajunga la a zecea coloana inainte sa proceseze urmatorul ~a
. ~t
nu consuma argumente. CL-USER> (format t "~a:~10t~a" :artist "Dixie Chicks") ARTIST: Dixie Chicks NILAcum lucrurile devin un pic mai complicate. Cand
FORMAT
intalneste ~{
urmatorul argument de consumat trebuie sa fie o lista. FORMAT
cicleaza prin lista, procesand directivele dintre ~{
si ~
} si consumand cate elemente din lista are nevoie de fiecare data. In dump-db
bucla FORMAT
va consuma cate un cuvant cheie si cate o valoare din lista la fiecare ciclu. Directiva ~%
nu consuma argumente ci il face pe FORMAT
sa treaca pe linia urmatoare. Apoi dupa ce ~
} termina bucla, ultimul ~%
il face pe FORMAT
sa treaca un rand mai jos pentru a delimita CD-urile.Tehnic, ai fi putut sa folosesti si
FORMAT
pentru a cicla prin baza de date, astfel functia dump-db
ar fi avut doar o linie.(defun dump-db () (format t "~{~{~a:~10t~a~%~}~%~}" *db*))Foarte fain sau foarte inspaimantator, depinde de punctul tau de vedere.
Imbunatatirea Interactiunii cu Utilizatorul
Functia
add-record
e buna pentru adaugarea de inregistrari, dar e un pic Lispeasca pentru utilizatorul obisnuit. Si daca cineva ar vrea sa adauge mai multe inregistrari, nu e prea comod. Deci ar fi buna o functie care sa ceara de la utilizator informatii despre un set de CD-uri. Acum stii ca ai nevoie de o modalitate de a-i cere utilizatorului niste informatii si a le citi.(defun prompt-read (prompt) (format *query-io* "~a: " prompt) (force-output *query-io*) (read-line *query-io*))Vom folosi vechiul nostru prieten
FORMAT
pentru a afisa linia de comanda. Deoarece nu exista ~%
in sirul de caractere de formatare, cursorul va ramane pe aceeasi linie. Apelul la FORCE-OUTPUT
este necesar in unele implementari pentru a ne asigura ca Lisp nu asteapta Enter (newline) inainte sa afiseze linia de comanda.Apoi poti sa citesti o linie individuala de text cu o functie foarte inspirat numita
READ-LINE
(citeste-linie). Variabila *query-io*
este o variabila globala (iti poti da seama deoarece foloseste conventia de denumire cu *
, folosita de variabilele globale) care contine fluxul de intrare conectat la terminat. Valoarea returnata de prompt-read
va fi valoarea din ultima forma, adica din apelul READ-LINE
, care returneaza sirul de caractere citit (fara Enter-ul de la sfarsit.)Poti sa combini functia
make-cd
cu prompt-read
pentru a construi o functie care creeaza o noua inregistrare pentru un CD de la datele primite de la tastatura.(defun prompt-for-cd () (make-cd (prompt-read "Title") (prompt-read "Artist") (prompt-read "Rating") (prompt-read "Ripped [d/n]")))Aproape gata. Doar ca
prompt-read
returneaza un string, care, desi e bun pentru campurile Title si Artist, nu e chiar atat de bun pentru Rating si Ripped, care trebuie sa fie un numar si un boolean. In functie de gradul de sofisticare al interfetei dorit, poti sa faci diverse variante de validare a datelor introduse de utilizator. Deocamdata hai sa facem ceva rapid: poti sa impachetezi prompt-read
intr-un apel la functia Lisp PARSE-INTEGER
:(parse-integer (prompt-read "Rating"))Din pacate, comportamentul implicit al
PARSE-INTEGER
este sa semnaleze o eroare daca nu poate sa extraga un numar din string-ul primit sau daca exista caractere non-numerice in string. Dar primeste un argument cuvant-cheie optional, :junk-allowed
, care ii spune sa se relaxeze un pic.(parse-integer (prompt-read "Rating") :junk-allowed t)Dar inca mai e o problema: daca nu gaseste un numar intreg in toate caracterele,
PARSE-INTEGER
returneaza NIL
in loc de un numar. Ca sa ramanem in continuare cu abordarea rapida o sa acceptam un 0 acolo si continuam. Macro-ul OR
este foarte bun aici. E similar cu operatorul ||
cu "scurt-circuit" din Perl, Python, Java, and C; primeste o serie de expresii, le evalueaza cate una si returneaza prima valoarea non-nil value (or NIL
daca sunt toate NIL
). Deci poti sa folosesti urmatoarea expresie:(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)pentru a primi o valoare implicita 0.
Repararea codului pentru a cere starea CD-ului (ripped? d/n) este mai simplu. Se poate folosi functia Common Lisp
Y-OR-N-P
.(y-or-n-p "Ripped [y/n]: ")De fapt, asta va fi cea mai robusta parte din
prompt-for-cd
, pentru ca Y-OR-N-P
va cere din nou informatie daca utilizatorul introduce ceva care nu incepe cu y, Y, n sau N.Din asamblarea tuturor acestor bucati rezulta o functie
prompt-for-cd
destul de robusta.(defun prompt-for-cd () (make-cd (prompt-read "Title") (prompt-read "Artist") (or (parse-integer (prompt-read "Rating") :junk-allowed t) 0) (y-or-n-p "Ripped [y/n]: ")))La urma, poti termina interfata "adauga niste CD-uri" impachetand
prompt-for-cd
intr-o functie care cicleaza pana cand utilizatorul a terminat. Poti sa folosesti cea mai simpla forma a macro-ului LOOP
, care executa repetat un corp de expresii pana cand se iese din el cu un apel la RETURN
. De exemplu: (defun add-cds () (loop (add-record (prompt-for-cd)) (if (not (y-or-n-p "Another? [y/n]: ")) (return))))Acum poti folosi
add-cds
ca sa adaugi inca niste CD-uri in baza de date.CL-USER> (add-cds) Title: Rockin' the Suburbs Artist: Ben Folds Rating: 6 Ripped [y/n]: y Another? [y/n]: y Title: Give Us a Break Artist: Limpopo Rating: 10 Ripped [y/n]: y Another? [y/n]: y Title: Lyle Lovett Artist: Lyle Lovett Rating: 9 Ripped [y/n]: y Another? [y/n]: n NIL
Salvarea si Incarcarea Bazei de Date
E bine ca avem un mod comod de a adauga inregistrari in baza de date. Dar nu e chiar asa bine daca utilizatorul va trebui sa introduca din nou toate inregistrarile de fiecare data cand opreste si porneste din nou Lisp. Din fericire, cu structurile de date folosite aici pentru reprezentarea datelor, e trivial sa se salveze datele intr-un fisier pentru a putea fi reincarcate mai tarziu. Urmeaza o functie
save-db
care primeste un nume de fisier ca argument si salveaza starea curenta a bazei de date:(defun save-db (filename) (with-open-file (out filename :direction :output :if-exists :supersede) (with-standard-io-syntax (print *db* out))))Macro-ul
WITH-OPEN-FILE
deschide un fisier, leaga fluxul la o variabila, executa un set de expresii si apoi inchide fisierul. Pe deasupra se si asigura ca fisierul este inchis chiar daca ceva nu merge cum trebuie in timpul evaluarii corpului. Lista imediat dupa WITH-OPEN-FILE
nu este un apel de functie ci este parte din sintaxa definita de WITH-OPEN-FILE
. Contine numele variabilei care va contine fluxul in care vei scris in corpul lui WITH-OPEN-FILE
, o valoare care trebuie sa fie un nume de fisier si apoi cateva optiuni care controleaza modul in care este deschis fisierul. Aici optiunile spun ca se deschide fisierul pentru scriere cu :direction :output
si ca se vrea suprascrierea unui fisier existent daca are acelasi nume cu cel transmis ca parametru, folosind :if-exists :supersede
. Odata ce fisierul e deschis, nu trebuie decat sa printezi continutul bazei de date cu
(print *db* out)
. Spre deosebire de FORMAT
, PRINT
printeaza obiecte Lisp intr-o forma din care pot fi citite inapoi de catre cititorul Lisp. Macro-ul WITH-STANDARD-IO-SYNTAX
se asigura ca anumite variabile care afecteaza comportamentul lui PRINT
contin valorile standard. Vei folosi acelasi macro cand vei citi datele inapoi pentru a fi sigur ca cititorul si printer-ul Lisp functioneaza compatibil.Argumentul catre
save-db
ar trebui sa fie un string continand numele fisierului in care utilizatorul doreste sa salveze baza de date. Forma exacta a string-ului va depinde de sistemul de operare folosit. De exemplu, pe un calculator Unix ar trebui sa poata apela save-db
astfel:CL-USER> (save-db "~/my-cds.db") ((:TITLE "Lyle Lovett" :ARTIST "Lyle Lovett" :RATING 9 :RIPPED T) (:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T) (:TITLE "Rockin' the Suburbs" :ARTIST "Ben Folds" :RATING 6 :RIPPED T) (:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 9 :RIPPED T))In Windows, numele de fisier ar putea sa fie "
c:/my-cds.db
" sau "c:\\my-cds.db
."4Poti deschide fisierul respectiv in orice editor de text pentru a vedea cum arata. Ar trebui sa vezi ceva foarte asemanator cu ce afiseaza REPL atunci cand tastezi
*db*
.Functia de incarcare a bazei de date in Lisp este similara.
(defun load-db (filename) (with-open-file (in filename) (with-standard-io-syntax (setf *db* (read in)))))De data asta nu trebuie sa specifici
:direction
in optiunile catre WITH-OPEN-FILE
, deoarece vrei optiunea implicita de :input
. Si in loc de printare, vei folosi functia READ
pentru a citi din fluxul in
. Este acelasi cititor cu cel din REPL si poate citi orice expresie Lisp ai introduce la linia de comanda. Totusi, in cazul de fata, doar citesti si afisezi expresia, nu o si evaluezi. Din nou, macro-ul WITH-STANDARD-IO-SYNTAX
se asigura ca READ
foloseste aceeasi sintaxa de baza ca si save-db
atunci cand a PRINT
-at datele.Macro-ul
SETF
este operatorul principal de atribuire in Common Lisp. El seteaza valoarea primului argument sa fie rezultatul obtinut din evaluarea celui de-al doilea argument. Deci in load-db
variabila *db*
va contine obiectul citit din fisier, adica lista de liste scrisa de save-db
. Trebuie sa fii atent in legatura cu un lucru - load-db
suprascrie eventualele date existente in *db*
de dinainte de apeld. Deci daca ai adaugat inregistrari cu add-record
sau add-cds
care nu au fost salvate cu save-db
, acestea vor fi sterse.Interogarea Bazei de Date
Acum ca dispui de un mod de a salva si reincarca baza de date impreuna cu o interfata comoda pentru adaugarea de inregistrari noi, in curand vei avea suficiente inregistrari incat sa nu mai vrei sa afisezi toata baza de date doar ca te uiti la ce-i prin ea. Ai nevoie de un mod de a interoga baza de date. Poate ti-ar placea, de exemplu, sa poti sa scrii ceva in genul:
(select :artist "Dixie Chicks")si sa vezi o lista cu toate inregistrarile in care artistul este Dixie Chicks. Vei vedea din nou ca salvarea inregistrarilor intr-o lista a fost o idee buna.
Functia
REMOVE-IF-NOT
primeste un predicat si o lista si returneaza o lista continand numai elementele care trec de predicat. Cu alte cuvinte, elimina toate elementele care nu trec de predicat. Totusi, REMOVE-IF-NOT
nu elimina nimic - el creeaza o lista noua, lasand lista originala neatinsa. Ca si cum ai rula grep pe un fisier. Argumentul predicat poate fi orice functie atata timp cat accepta un singur argument si returneaza o valoare booleana - NIL
pentru fals si orice altceva pentru adevarat.De exemplu, daca ai vrea sa extragi toate elementele pare dintr-o lista de numere, ai putea folosi
REMOVE-IF-NOT
astfel:CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10)In acest caz, predicatul este functia
EVENP
, care returneaza adevarat daca argumentul este un numar par. Ratingtia #'
e o scurtatura pentru "Da-mi functia cu urmatorul nume". Fara
#'
, Lisp ar trata evenp
ca fiind numele unei variabile si ar cauta valoarea unei variabile, nu a unei functii.Poti sa ii transmiti si o functie anonima lui
REMOVE-IF-NOT
. De exemplu, daca EVENP
nu ar fi existat, ai fi putut scrie expresia anterioara astfel:CL-USER> (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10)In acest caz, predicatul este functia anonima:
(lambda (x) (= 0 (mod x 2)))care verifica daca restul impartirii argumentului la 2 este egal cu 0 (adica e par). Daca ai vrea sa extragi numai numerele impare folosind o functie anonima. ai scrie asa:
CL-USER> (remove-if-not #'(lambda (x) (= 1 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10)) (1 3 5 7 9)De observat ca
lambda
nu este nume de functie - este ceea ce indica faptul ca tu definesti o functie anonima.5 In afara de lipsa unui nume, o expresie LAMBDA
seamana foarte mult cu DEFUN
: cuvantul lambda
este urmat de o lista de parametri, care este urmata de corpul functiei. Pentru a selectat toate albumele Dixie Chicks din baza de date folosind
REMOVE-IF-NOT
, ai nevoie de o functie care sa returneze adevarat atunci cand campul artistului din inregistrare este "Dixie Chicks"
. Nu uita ca am ales sa reprezentam inregistrarile prin plist-uri deoarece functia GETF
poate sa extraga campuri din plist dupa nume. Deci, pentru o variabila numita cd
care contine o singura inregistrare, poti folosi expresia (getf cd :artist)
pentru a extrage numele artistului. Functia EQUAL
, cand primeste argumente siruri de carcatere le compara caracter cu caracter. Deci (equal (getf cd :artist) "Dixie Chicks")
testeaza daca numele artistului dintr-o inregistrare este egal cu "Dixie Chicks"
. Nu trebuie decat sa impachetezi expresia intr-o forma LAMBDA
pentru a face o functie anonima si a o transmite catre REMOVE-IF-NOT
.CL-USER> (remove-if-not #'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks")) *db*) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))Acum sa zicem ca vrei sa impachetezi toata expresia intr-o functie care primeste numele unui artist ca argument. Poti sa scrii asa:
(defun select-by-artist (artist) (remove-if-not #'(lambda (cd) (equal (getf cd :artist) artist)) *db*))De observat ca functia anonima, care contine cod care nu va rula pana nu va fi apelata in
REMOVE-IF-NOT
, poate sa refere totusi variabila artist
. In acest caz functia anonima nu doar te scuteste de la a scrie o functie obisnuita - te lasa sa scrii o functie care deriveaza parte din intelesul ei - valoare lui artist
- din contextul in care e inclusa.Deci asta e
select-by-artist
. Totusi, sa cauti dupa artist este doar unul din tipurile de interogari pe care vrei sa le suporti. Ai putea sa mai scrii niste functii, cum ar fi select-by-title
, select-by-rating
, select-by-title-and-artist
si mai departe. Dar ar fi foarte asemanatoare cu exceptia continutului functiei anonime. Poti in loc de asta sa faci o functie select
mai generala care primeste o functie ca argument.(defun select (selector-fn) (remove-if-not selector-fn *db*))Ce s-a intamplat cu
#'
? Pai, in cazul de fata nu vrei ca REMOVE-IF-NOT
sa foloseasca functia numita selector-fn
. Vrei sa foloseasca functia anonima care a fost transmisa lui select
ca argument in variabila selector-fn
. Desi, #'
se intoarce in apelul catre select
.CL-USER> (select #'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks"))) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))Arata cam aiurea. Din fericire, poti sa impachetezi crearea functiei anonime.
(defun artist-selector (artist) #'(lambda (cd) (equal (getf cd :artist) artist)))Asta e o functie care returneaza o functie si care refera o variabila care - aparent - nu va exista dupa returnarea din
artist-selector
.6 Poate sa para ciudat acum, dar chiar functioneaza cum trebuie - daca apelezi artist-selector
cu argumentul "Dixie Chicks"
, vei primi o functie anonima care poate sa gaseasca CD-urile al caror camp :artist
este "Dixie Chicks"
, si daca o apelezi cu "Lyle Lovett"
, vei primi o functie diferita care va cauta :artist
-ul "Lyle Lovett"
. Deci acum poti sa rescrii apelul catre select
astfel:CL-USER> (select (artist-selector "Dixie Chicks")) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))Acum nu mai trebuie decat niste functii care sa genereze selectori. Dar la fel cum nu vrei sa scrii
select-by-title
, select-by-rating
si restul pentru ca ar fi foarte similare, la fel nu vrei sa scrii cateva generatoare de functii selector aproape identice, pentru fiecare camp. De ce sa nu scrii un generator de functii-selector general, o functie care, in functie de argumente, sa genereze o functie-selector pentru diferite campuri sau poate chiar pentru combinatii de campuri? Poti sa scrii astfel de functii, dar mai intai ai nevoie de o introducere rapida intr-o capabilitate numita keyword parameters (parametri cuvinte-cheie).In functiile scrise pana acum ai specificat o lista simpla de parametri, care sunt legati la argumentele corespunzatoare in apelul catre functie. De exemplu, functia urmatoare:
(defun foo (a b c) (list a b c))are trei parametri,
a
, b
si c
, si trebuie apelata cu trei argumente. Dar uneori poate vrei sa scrii o functie care sa poata fi apelata cu numar variabil de argumente. Parametrii cuvinte-cheie sunt o modalitate de a realiza lucrul acesta. O versiune a foo
care foloseste parametri cuvinte-cheie ar putea arata asa: (defun foo (&key a b c) (list a b c))Singura diferenta este
&key
de la inceputul listei de argumente. Cu toate astea, apelurile la noua foo
vor arata destul de diferit. Apelurile urmatoare sunt legale, rezultatele fiind afisate in dreapta semnelor ==>:(foo :a 1 :b 2 :c 3) ==> (1 2 3) (foo :c 3 :b 2 :a 1) ==> (1 2 3) (foo :a 1 :c 3) ==> (1 NIL 3) (foo) ==> (NIL NIL NIL)Dupa cum arata aceste exemple, valorile variabilelor
a
, b
si c
sunt legate la valorile care urmeaza cuvantului cheie corespunzator. Si daca un anumit cuvant cheie nu apare in apel, variabila corespunzatoare este pusa NIL
. Voi trece peste unele din detaliile despre cum trebuie specificati parametrii cuvinte-cheie si ce legatura au cu alte tipuri de parametri, dar trebuie sa mai stii un detaliu.In mod normal, daca o functie este apelata fara argument pentru un anumit parametru cuvant cheie, parametrul va avea valoarea
NIL
. Dar uneori vrei sa faci distinctie intre un NIL
care a fost transmis explicit ca argument intr-un parametru cuvant-cheie si valoarea implicita NIL
. Pentru a permite asta, cand specifici un parametru cuvant-cheie poti sa inlocuiesti numele simplu cu o lista formata din numele parametrului, o valoare implicita, si inca un nume de parametru, numit un parametru supplied-p (furnizat-p). Parametrul supplied-p va fi setat adevarat daca un argument chiar a fost transmis pentru parametrul cuvant-cheie respectiv sau nu in apelul catre functie. Uite o versiune a lui foo
care foloseste aceasta capabilitate:(defun foo (&key a (b 20) (c 30 c-p)) (list a b c c-p))Daca repetam acum apelurile de mai devreme vom avea alte rezultate:
(foo :a 1 :b 2 :c 3) ==> (1 2 3 T) (foo :c 3 :b 2 :a 1) ==> (1 2 3 T) (foo :a 1 :c 3) ==> (1 20 3 T) (foo) ==> (NIL 20 30 NIL)Generatorul de functii-selector general, pe care il poti numi
where
din motive pe care le vei vedea in curand daca esti familiar cu bazele de date SQL este o functie care primeste patru parametri cuvinte-cheie corespunzatori campurilor din inregistrarile noastre si genereaza o functie selector care cauta orice CD-uri care se potrivesc cu toate valorile date lui where
. De exemplu, te lasa sa spui lucruri ca asta: (select (where :artist "Dixie Chicks"))sau asta:
(select (where :rating 10 :ripped nil))Functia este definita astfel:
(defun where (&key title artist rating (ripped nil ripped-p)) #'(lambda (cd) (and (if title (equal (getf cd :title) title) t) (if artist (equal (getf cd :artist) artist) t) (if rating (equal (getf cd :rating) rating) t) (if ripped-p (equal (getf cd :ripped) ripped) t))))Functia asta returneaza o functie anonima care returneaza
SI
logic al cate unei clauze per camp din inregistrarile noastre. Fiecare clauza verifica daca argumentul propriu a fost transmis si apoi il compara cu valoarea din campul corespunzator din inregistrare sau returneaza t
, "adevarat" in Lisp, daca parametrul nu a fost transmi. Deci functia selector va returna t
numai pentru CD-urile care confirma toate argumentele transmise lui where
.7 De observat ca trebuie folosita o lista cu trei elemente pentru a specifica parametrul cuvant-cheie ripped
deoarece ai nevoie sa stii daca apelantul chiar a trimis :ripped nil
, adica "Selecteaza CD-urile ale caror camp ripped este nil," sau daca au omis :ripped
, adica "Nu ma intereseaza valoarea campului ripped." Modificarea Inregistrarilor Existente - Un Alt Uz pentru
Acum ai functii
select
si where
generalizate, deci poti sa scrii urmatoarea caracteristica pe care o necesita orice baza de date - un mod de a modifica inregistrarile dorite. In SQL comanda update
este folosita pentru a modifica un set de inregistrari care se potrivesc cu o clauza where
. Asta pare a fi un model bun, mai ales deoarece ai scris deja un generator de clauze where
. De fapt, functia update
este mai mult o aplicare a unor idei deja vazute: foloseste o functie selector transmisa pentru a alege inregistrarile de modificat folosind parametri cuvinte-cheie pentru a specifica valorile de schimbat. Noutatea este folosirea funcitei MAPCAR
care trece peste o lista, *db*
in cazul de fata si returneaza o noua lista continand rezultatele apelarii functiei pentru fiecare element din lista originala.(defun update (selector-fn &key title artist rating (ripped nil ripped-p)) (setf *db* (mapcar #'(lambda (row) (when (funcall selector-fn row) (if title (setf (getf row :title) title)) (if artist (setf (getf row :artist) artist)) (if rating (setf (getf row :rating) rating)) (if ripped-p (setf (getf row :ripped) ripped))) row) *db*)))Inca o noutate aici este utilizarea lui
SETF
pe o forma complexa ca (getf row :title)
. Voi discuta despre SETF
mai detaliat in Capitolul 6, dar deocamdata trebuie sa stii doar ca este un operator de atribuire general care poate fi folosit pentru a atribui in multe alte "locuri" pe langa variabile. (E o coincidenta asemanarea numelor SETF
si GETF
- nu sunt legate in vreun fel). Deocamdata este suficient de stiut ca dupa (setf (getf row :title) title)
, plist-ul referit de row va avea valoarea variabilei title
dupa numele :title
. Cu aceasta functie update
daca te hotarasti iti plac foarte mult Dixie Chicks si ca toate albumele lor ar trebui sa aiba nota 11, poti sa evaluezi forma urmatoare: CL-USER> (update (where :artist "Dixie Chicks") :rating 11) NILAnd it is so.
CL-USER> (select (where :artist "Dixie Chicks")) ((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 11 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 11 :RIPPED T))Ba chiar e mai usor sa adaugi o functie care sa stearga randuri din baza de date.
(defun delete-rows (selector-fn) (setf *db* (remove-if selector-fn *db*)))Functia
REMOVE-IF
este complementul lui REMOVE-IF-NOT
; returneaza o lista fara toate elementele din lista originala care trec de predicat. La fel cu REMOVE-IF-NOT
, nu afecteaza lista transmisa ca parametru ci, salvand rezultatul in *db*
, delete-rows
8 chiar schimba continutul bazei de date.9Cum sa Scapi de Redundanta si sa Castigi Baban
Deocamdata tot codul pentru implementarea introducerii, cautarii, modificarii si stergerii, ca sa nu mentionam si o interfata de linie de comanda pentru adaugarea de noi inregistrari si salvarea bazei de date, se aduna la un pic peste 50 de linii. Total.10
Si totusi inca mai exista redundanta in cod. Si o sa vezi ca poti sa scapi de reduntanta si sa faci si codul mai flexibil. Redundanta de care vorbesc se afla in functia where. Corpul functiei
where
este alcatuit din cate o clauza pe camp, fiecare in genul asta:(if title (equal (getf cd :title) title) t)Deocamdata nu e prea rau, dar la fel cu orice redundanta in cod are un cost: daca vrei sa schimbi modul de functionare trebuie sa schimbi in mai multe locuri. Si daca schimbi campurile dintr-un CD, va trebui sa adaugi sau sa scoti clauze din
where
. Si update
sufera de acelasi tip de redundanta. E de doua ori mai rau deoarece tot scopul functiei where
este sa genereze automat un pic de cod care verifica valorile de care ai tu nevoie; de ce trebuie sa lucreze in timpul rularii sa verifice chiar si daca title
a fost transmis?Imagineaza-ti ca ai incerca sa optimizezi codul si ai descoperi ca se cheltuieste prea mult timp verificand daca
title
si restul parametrilor cuvinte-cheie din where
contin macar valoari11 Daca vrei intr-adevar sa scapi de toate verificarile din timpul executiei, ai putea sa treci printr-un program si sa cauti toate locurile in care apelezi where
si sa vezi exact ce parametri ii transmiti. Apoi ai putea sa inlocuiesti fiecare apel catre where
cu o functie anonima care nu face decat calculele necesare. De exemplu, daca ai gasi bucatica asta de cod:(select (where :title "Give Us a Break" :ripped t))ai putea s-o schimbi astfel:
(select #'(lambda (cd) (and (equal (getf cd :title) "Give Us a Break") (equal (getf cd :ripped) t))))Functia anonima e diferita de cea pe care ar fi returnat-o
where
; nu incerci sa salvezi apelul la where
ci mai degraba sa faci o functie selector mai eficienta. Aceasta functie anonima are clauze numai pentru campurile de care chiar ai nevoie in acest loc, deci nu face treaba in plus, cum ar fi facut functia returnata de where
.Probabil ca-ti imaginezi ca ar trebui sa repari toate apelurile catre
where
in stilul asta. Dar poti sa-ti imaginezi si ca asta ar fi o mare bataie de cap. Daca ar fi suficient de multe si suficient de importanta schimbarea, poate chiar ar merita sa scrii un fel de preprocesor pentru a converti apelurile la where
in codul dorit de tine.Capabilitatea Lisp care face ca acest lucru sa fie foarte usor este sistemul de macro-uri. Nu pot sa subliniez destul faptul ca un macro Common Lisp nu seamana decat ca nume cu un macro din C si C++. Pre-procesorul C functioneaza prin inlocuire de text, fara a intelege aproape nimic din structura C si C++, un macro Lisp este ca idee un generator de cod care e rulat automat de compilator.12 Cand o expresie Lisp contine un apel catre un macro, in loc sa-i evalueze argumentele si sa le transmita functiei, compilatorul Lisp transmite argumentele, neevaluate, codului macro-ului, iar acesta returneaza o expresie Lisp noua care este evaluata in locul codului original.
Voi incepe cu un exemplu simplu si usor si apoi iti voi arata cum poti sa inlocuiesti functia
where
cu macro-ul where
. Inainte sa scriu acest macro drept exemplu, trebuie sa introduc rapid o functie noua: REVERSE
primeste o lista ca argument si returneaza lista inversata intr-o lista noua. Deci (reverse '(1 2 3))
returneaza (3 2 1)
. Acum hai sa cream un macro. (defmacro backwards (expr) (reverse expr))Principala diferenta sintactica dintre o functie si un macro este ca macro-ul se defineste cu
DEFMACRO
in loc de DEFUN
. In continuare urmeaza un nume, la fel ca la functie, o lista de parametri si un corp de expresii, tot ca la functii. Dar un macro are un efect diferit. Poti sa folosesti acest macro in felul urmator:CL-USER> (backwards ("hello, world" t format)) hello, world NILCum a functionat asta? Cand REPL a inceput sa evalueze expresia
backwards
, a recunoscut ca backwards
este numele unui macro. Deci a lasat expresia ("hello, world" t format)
neevaluata, ceea ce e bine deoarece nu e forma legala in Lisp. Apoi a transmis lista lui backwards
. Codul din backwards
a transmis lista lui REVERSE
, care a returnat lista (format t "hello, world")
. backwards
a transmis apoi valoarea inapoi in REPL, unde a fost evaluata in locul expresiei originale.Macro-ul
backwards
defineste un nou limbaj care seamana mult cu Lisp - doar ca se scrie pe dos - pe care poti sa-l folosesti oricand pur si simplu impachetand o expresie Lisp scrisa invers intr-un apel la macro-ul backwards
. Intr-un program Lisp compilat acest nou limbaj este la fel de eficient ca si Lisp-ul normal deoarece tot codul macro - adica cel care genereaza expresia noua - ruleaza la momentul compilarii. Cu alte cuvinte, compilatorul va genera exact acelasi cod indiferent daca scrii (backwards ("hello, world" t format))
sau (format t
"hello, world")
.Si cum ma ajuta pe mine sa scap de redundanta de cod din
where
? Pai, poti scrie un macro care genereaza exact codul de care ai nevoie pentru fiecare apel la where
. Iarasi, cea mai buna abordare este sa scrii codul incepand de jos (bottom up). In functia selector scrisa initial, exista cate o expresie scrisa in felul urmator pentru fiecare camp referit in apelul original la where
:(equal (getf cd camp) valoare)Hai sa scriem o functie care, cand primeste numele unui camp si o valoare, returneaza o astfel de expresie. Deoarece o expresie este doar o lista, poate te gandesti ca poti sa scrii ceva in genul asta:
(defun make-comparison-expr (field value) ; gresit (list equal (list getf cd field) value))Totusi, e un schepsis aici: dupa cum stii, cand Lisp vede un nume simplu ca
field
sau value
in alta parte decat ca prim element al unei liste, presupune ca este o variabila si ii cauta valoarea. Asta merge pentru field
si value
; e exact ce vrei. Dar va trata equal
, getf
si cd
la fel, ceea ce nu este de dorit. Dar, stii si cum sa opresti pe Lisp sa evalueze o forma: pune-i un apostrof ('
) in fata. Deci daca scrii make-comparison-expr
dupa cum urmeaza, va face ceea ce vrei:(defun make-comparison-expr (field value) (list 'equal (list 'getf 'cd field) value))Poti sa testezi in REPL.
CL-USER> (make-comparison-expr :rating 10) (EQUAL (GETF CD :RATING) 10) CL-USER> (make-comparison-expr :title "Give Us a Break") (EQUAL (GETF CD :TITLE) "Give Us a Break")De fapt exista un mod si mai bun de a face asta. Ce ai vrea este sa stii un mod in care poti scrie o expresie care este neevaluata in mare parte si apoi sa evaluezi cateva expresii din ea. Si, desigur, exista mecanism pentru asa ceva. Un apostrof invers (backquote - ???) - (
`
) inaintea unei expresii opreste evaluarea la fel ca un apostrof normal.CL-USER> `(1 2 3) (1 2 3) CL-USER> '(1 2 3) (1 2 3)Totusi, intr-o expresie citata cu
`
, orice subexpresie precedata de o virgula este evaluata. Uite ce efect are virgula in a doua expresie:`(1 2 (+ 1 2)) ==> (1 2 (+ 1 2)) `(1 2 ,(+ 1 2)) ==> (1 2 3)Folosind un "back quote" poti sa scrii
make-comparison-expr
astfel: (defun make-comparison-expr (field value) `(equal (getf cd ,field) ,value))Acum daca te uiti la functia selector optimizata de mana, poti sa vezi ca in corpul functiei apare cate o expresie de comparare pentru fiecare pereche camp/valoare, toate impachetate intr-o expresie
AND
. Sa presupunem pentru un moment ca aranjezi sa transmiti argumentele macro-ului ca o singura lista. Ai nevoie de o functie care sa "sparga" elementele unei astfel de lista pe perechi si sa colecteze rezultatele apelarii lui make-comparison-expr
pentru fiecare pereche. Pentru a implementa functia, poti sa te uiti in gentuta cu trucuri avansate de Lisp si sa scoti macro-ul LOOP
cel tare.(defun make-comparisons-list (fields) (loop while fields collecting (make-comparison-expr (pop fields) (pop fields))))O discutie pe indelete despre
LOOP
va trebui sa astepte pana in Capitolul 22; deocamdata poti sa vezi ca aceasta expresie LOOP
face exact ce ai nevoie: cicleaza prin elementele din lista fields
, extragand cate doua, transmitandu-le lui make-comparison-expr
si colectand rezultatele la sfarsitul buclei. Macro-ul POP
face operatia inversa macro-ului PUSH
folosit pentru a adauga inregistrari in *db*
.Acum nu mai e nevoie decat sa impachetezi lista returnata de
make-comparison-list
intr-un AND
si intr-o functie anonima, ceea ce se poate face si in macro-ul where
. E foarte simplu daca folosesti un apostrof invers (???) si faci un sablon pe care-l umpli interpoland valoarea lui make-comparisons-list
.(defmacro where (&rest clauses) `#'(lambda (cd) (and ,@(make-comparisons-list clauses))))Acest macro foloseste o varianta de
,
(mai exact the ,@
) inainte de apelul la make-comparisons-list
. ,@
"sparge" valoarea urmatoarei expresii - care trebuie sa fie o lista - in lista din exterior. Poti sa vezi diferenta dintre ,
si ,@
in urmatoarele expresii: `(and ,(list 1 2 3)) ==> (AND (1 2 3)) `(and ,@(list 1 2 3)) ==> (AND 1 2 3)Poti sa folosesti
,@
si pentru a sparge in mijlocul unei liste.`(and ,@(list 1 2 3) 4) ==> (AND 1 2 3 4)Cealalta caracteristica importanta a macro-ului
where
este folosirea lui &rest
in lista de argumente. La fel cu &key
, &rest
modifica modul in care sunt analizate (parsed) argumentele. O functie sau macro care au &rest
in lista de parametri pot sa primeasca un numar arbitrar de argumente, pe care le colecteaza intr-o lista unica ce devine valoarea variabilei al carei nume urmeaza clauza &rest
. Deci daca apelezi where
asa:(where :title "Give Us a Break" :ripped t)variabila
clauses
va contine lista.(:title "Give Us a Break" :ripped t)Aceasta lista e transmisa lui
make-comparisons-list
, care returneaza o lista de expresii de comparare. Poti sa vezi exact codul generat de un apel la where
cu functia MACROEXPAND-1
.Daca ii transmiti lui
MACROEXPAND-1
o forma reprezentand apelul unui macro, el va apela macro-ul cu argumente potrivite si va returna expansiunea rezultata. Deci poti sa verifici apelul anterior la where
astfel: CL-USER> (macroexpand-1 '(where :title "Give Us a Break" :ripped t)) #'(LAMBDA (CD) (AND (EQUAL (GETF CD :TITLE) "Give Us a Break") (EQUAL (GETF CD :RIPPED) T))) TArata bine. Hai sa-l incercam pe bune.
CL-USER> (select (where :title "Give Us a Break" :ripped t)) ((:TITLE "Give Us a Break" :ARTIST "Limpopo" :RATING 10 :RIPPED T))Functioneaza. Si macro-ul
where
impreuna cu cele doua functii ajutatoare au o linie de cod mai putin fata de functia veche where
. Si este mai general, adica nu este legat de campurile specifice din inregistrarile noastre. In Incheiere
S-a intamplat un lucru interesant. Ai scapat de redundanta si ai facut codul mai eficient si mai general in acelasi timp. Se intampla des cu un macro bine ales. Asta are sens doarece un macro este doar un alt mecanism pentru crearea abstractiunilor - la nivel sintactic, si abstractiunile sunt prin definitie moduri mai concise de a exprima generalitatile de baza. Acum singurele bucati de cod din mini-baza de date specifice CD-urilor si informatiilor despre ele sunt in functiile
make-cd
, prompt-for-cd
si add-cd
. De fapt, noul macro where
ar putea sa functioneze cu orice baza de date care are plist-uri ca implementare.Dar, este o distanta mare pana sa fie o baza de date completa. Probabil te poti gandi la o gramada de facilitati pe care le-ai putea adauga, cum ar fi sa suporte tabele multiple sau interogari mai elaborate. In Capitolul 27 vom construi o baza de date MP3-uri care incorporeaza cateva din aceste facilitati.
Scopul acestui capitol a fost sa iti dea o scurta introducere in cateva din capabilitatile Lisp-ului si sa-ti arate cum pot fi folosite pentru a scrie cod ceva mai interesant decat "hello, world". In capitolul urmator vom incepe un studiu mai sistematic al Lisp-ului.
1Inainte sa incep, este foarte important sa uiti tot ce ai putea stii despre "macro"-urile stil #define implementate in preprocesorul C. Macrourile Lisp sunt cu totul alta dihanie.
2Folosirea unei variabile globale are si neajunsuri - de exemplu nu poti avea decat o singura baza de date la un moment dat. In Capitolul 27, cunoscand o parte mai mare din limbaj, vei putea sa construiesti o baza de date mai flexibila. In Capitolul 6 vei vedea cum pana si variabilele globale sunt mai flexibile in Common Lisp decat in alte limbaje.
3Una din cele mai tari directive
4Windows stie si slash-uri in numele de fisiere desi in mod normal foloseste backslash ca separator de directoare. E comod deoarece altfel ar trebui sa scrii backslash-uri de cate doua ori, backslash-ul fiind caracterul de "evadare" in string-urile Lisp.
5Cuvantul lambda este folosit in Lisp datorita unei legaturi mai vechi cu calculul lambda, un formalism matematic inventat pentru studiul functiilor matematice.
6Termenul tehnic pentru o functie care refera o variabila din domeniul de definitie exterior este inchidere deoarece functia "inchide" variabil. Voi discuta despre inchideri mai detaliat in Capitolul 6.
7De observat ca in Lisp o forma IF, la fel cu orice altceva, e o expresie care returneaza o valoare. Seamana mai degraba cu operatorul ternar (
8Trebuie sa folosesti numele
9Daca te temi ca aici ar putea sa apara scurgeri de memorie, stai linistit: Lisp a inventat colectarea gunoiului (garbage collection) - si alocarea pe heap daca tot vorbim despre asta. Memoria folosita de valoarea veche a
10Un prieten intervieva un inginer pentru un job in programare si i-a pus intrebarea tipica de interviu: cum stii cand o functie sau metoda este prea mare? Pai, a raspuns candidatul, nu-mi place sa am metode mai mari decat capul meu. Adica sa nu poti pastra toate detaliile in cap? Nu, imi pun capul pe monitor si codul n-ar trebui sa fie mai mare decat capul meu.
11E putin probabil sa conteze mult in performanta costul verificarii transmiterii parametrilor cuvant-cheie deoarece compararea unei variabile cu
12Macro-urile sunt rulate si de catre interpretor - dar e mai usor sa intelegi scopul macro-urilor cand te gandesti la cod compilat. Ca si cu toate celelalte notiuni din capitol, le voi acoperi mai detaliat in capitole viitoare.
2Folosirea unei variabile globale are si neajunsuri - de exemplu nu poti avea decat o singura baza de date la un moment dat. In Capitolul 27, cunoscand o parte mai mare din limbaj, vei putea sa construiesti o baza de date mai flexibila. In Capitolul 6 vei vedea cum pana si variabilele globale sunt mai flexibile in Common Lisp decat in alte limbaje.
3Una din cele mai tari directive
FORMAT
este directiva ~R
. Ai vrut sa afli vreodata cum se citeste un numar foarte mare in cuvinte in limba engleza? Lisp stie. Evalueaza expresia urmatoare:(format nil "~r" 1606938044258990275541962092)si ar trebui sa afiseze:
"one octillion six hundred six septillion nine hundred thirty-eight(nu traduc asta nici sa ma tai)
sextillion forty-four quintillion two hundred fifty-eight
quadrillion nine hundred ninety trillion two hundred seventy-five
billion five hundred forty-one million nine hundred sixty-two
thousand ninety-two"
4Windows stie si slash-uri in numele de fisiere desi in mod normal foloseste backslash ca separator de directoare. E comod deoarece altfel ar trebui sa scrii backslash-uri de cate doua ori, backslash-ul fiind caracterul de "evadare" in string-urile Lisp.
5Cuvantul lambda este folosit in Lisp datorita unei legaturi mai vechi cu calculul lambda, un formalism matematic inventat pentru studiul functiilor matematice.
6Termenul tehnic pentru o functie care refera o variabila din domeniul de definitie exterior este inchidere deoarece functia "inchide" variabil. Voi discuta despre inchideri mai detaliat in Capitolul 6.
7De observat ca in Lisp o forma IF, la fel cu orice altceva, e o expresie care returneaza o valoare. Seamana mai degraba cu operatorul ternar (
?:
) din Perl, Java si C in sensul ca urmatoarea expresie e legala in aceste limbaje:some_var = some_boolean ? value1 : value2;in timp ce urmatoarea nu este:
some_var = if (some_boolean) value1; else value2;deoarece in acele limbaje
if
este instructiune, nu expresie.8Trebuie sa folosesti numele
delete-rows
in loc de delete
deoarece exista deja o functie in Common Lisp numita DELETE
. Sistemul de pachete Lisp iti da mai multe posibilitati de a administra conflictele de nume, incat ai putea sa ai si o functie numita delete daca ai vrea. Dar inca nu e cazul sa discutam despre pachete.9Daca te temi ca aici ar putea sa apara scurgeri de memorie, stai linistit: Lisp a inventat colectarea gunoiului (garbage collection) - si alocarea pe heap daca tot vorbim despre asta. Memoria folosita de valoarea veche a
*db*
va fi recuperata automat, presupunand ca nu exista alte referinte catre ea, si in codul nostru nu exista.10Un prieten intervieva un inginer pentru un job in programare si i-a pus intrebarea tipica de interviu: cum stii cand o functie sau metoda este prea mare? Pai, a raspuns candidatul, nu-mi place sa am metode mai mari decat capul meu. Adica sa nu poti pastra toate detaliile in cap? Nu, imi pun capul pe monitor si codul n-ar trebui sa fie mai mare decat capul meu.
11E putin probabil sa conteze mult in performanta costul verificarii transmiterii parametrilor cuvant-cheie deoarece compararea unei variabile cu
NIL
e destul de ieftin. Pe de alta parte, functiile returnate de where
se vor afla fix in mijlocul buclei interne al oricarui apel catre select
, update
sau delete-rows
, acestea trebuind apelate cate o data pentru fiecare inregistrare din baza de date. Oricum, ne multumim cu asta, ca demonstratie.12Macro-urile sunt rulate si de catre interpretor - dar e mai usor sa intelegi scopul macro-urilor cand te gandesti la cod compilat. Ca si cu toate celelalte notiuni din capitol, le voi acoperi mai detaliat in capitole viitoare.
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.