Pagini

2012-12-01

Practical Common Lisp - 3. Practic: O Baza de Date Simpla

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)
3
Acestea 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:   T
Functia 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
NIL
sau:
CL-USER> (format t "~a" :title)
TITLE
NIL
Directiva ~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
NIL
Acum 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."4
Poti 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)
NIL
And 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-rows8 chiar schimba continutul bazei de date.9

Cum 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
NIL
Cum 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)))
T
Arata 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 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
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"
(nu traduc asta nici sa ma tai)
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.