Copyright © Peter Seibel
Traducere © Razvan Popa
5. Functii
(pe scurt ▼)
Dupa regulile de sintaxa si semantica, cele mai de baza trei componente ale programelor Lisp sunt functiile, variabilele si macro-urile. Le-ai folosit pe toate in timp ce construiai baza de date din Capitolul 3, dar am trecut peste multe din detaliile de functionare si folosire. Voi dedica urmatoarele cateva capitole acestor trei subiecte, incepand cu functiile, care - la fel ca omoloagele lor din alte limbaje - constituie mecanismul de baza pentru abstractizarea functionalitatii.
Mare parte din Lisp insusi sunt functii. Mai mult de trei sferturi din numele definite in standardul limbajului sunt ale functiilor. Toate tipurile de date interne (built-in) sunt definite in intregime pe baza functiilor care opereaza cu ele. Chiar si puternicul sistem de obiecte din Lisp este construit pe o extensie conceptuala a functiilor, functiile generice, despre care voi vorbi in Capitolul 16.
Si, in ciuda importantei macro-urilor pentru Calea Lisp, pana la urma toata functionalitatea reala este data de functii. Macro-urile ruleaza in momentul compilarii, deci codul pe care il genereaza - si care va alcatui programul propriu-zis dupa expandarea macro-urilor - va consta numai din apeluri catre functii si operatori speciali. Fara sa mentionam ca macro-urile sunt tot functii, chiar daca functii care sunt folosite la generarea de cod in loc de executarea de actiuni in program.1
Cum Se Definesc Functii Noi
In mod normal functiile sunt definite cu macro-ul
DEFUN
. Scheletul de baza al unui DEFUN
arata astfel:(defun nume (parametru*) "Sir de caractere documentar optional." forma-corp*)Orice simbol poate fi folosit ca nume de functie.2 De obicei numele de functii contin doar caractere alfabetice si cratime, dar sunt permise si alte caractere, si chiar sunt folosite in anumite conventii de numire. De exemplu functiile care convertesc un tip de valoare intr-altul folosesc uneori
->
in nume. O functie care converteste string-uri in widget-uri ar putea fi numita string->widget
. Cea mai importanta conventie de numire este cea mentionata in Capitolul 2, care cere sa se construiasca numele compuse folosind cratime in loc de underscore sau majuscule interne. Deci, fa-ceva
e mai aproape de stilul Lisp decat fa_ceva
sau faCeva
.Lista de parametri a unei functii defineste variabilele care vor fi folosite pentru memorarea argumentelor transmise functiei la momentul apelului.3 Daca functia nu are parametri, lista este vida, scrisa astfel:
()
. Diferite tipuri de parametri se ocupa de argumentele obligatorii, optionale, multiple sau cu cuvinte cheie. Voi discuta detaliile in sectiunea urmatoare.Daca un sir de caractere literal urmeaza listei de parametri, este privit ca sir de caractere documentar si ar trebui sa descrie scopul functiei. La definirea functiei string-ul documentar este asociat cu numele functiei si poate fi obtinut mai devreme folosind functia
DOCUMENTATION
.4In sfarsit, corpul unui
DEFUN
consta din orice numar de expresii Lisp. Acestea vor fi evaluate in ordine cand functia e apelata si valoarea ultimei expresii este returnata ca valoarea functiei. Sau se poate folosi operatorul special RETURN-FROM
pentru a returna din functie in orice moment, dupa cum vom vedea in scurt timp.In Capitolul 2 am scris o functie
hello-world
, care arata astfel:(defun hello-world () (format t "hello, world"))Acum poti sa analizezi partile componente ale functiei. Numele este
hello-world
, lista de parametri e vida deci nu primeste parametri, nu are string de documentare si corpul este doar o expresie. (format t "hello, world")Urmeaza o functie un pic mai complexa:
(defun verbose-sum (x y) "Insumeaza oricare doua numere dupa ce afiseaza un mesaj." (format t "Insumez ~d si ~d.~%" x y) (+ x y))Functia este numita
verbose-sum
, primeste doua argumente care vor fi legate la parametrii x
si y
, are un string de documentatie si are un corp care contine doua expresii. Valoarea returnata de apelul la +
devine valoarea returnata de verbose-sum
. Liste de Parametri ale Functiilor
Nu mai e mult de spus despre nume de functii sau string-uri de documentatie, si va fi necesara o buna parte din aceasta carte pentru a descrie toate lucrurile care pot fi facuta in corpul unei functii, deci deocamdata ne mai ocupam doar de lista de parametri.
Scopul de baza a listei de parametri este, desigur, sa declare variabilele care vor primi argumentele transmise functiei. Cand o lista de parametri este o lista simpla de nume - ca in
verbose-sum
- parametrii se numesc parametri obligatorii. Cand este apelata o functie, trebuie sa i se transmita cate un argument pentru fiecare dintre acesti parametri obligatorii. Fiecare parametru este legat de argumentul corespunzator. Daca o functie este apelata cu prea putine sau prea multe argumente, Lisp va semnala eroare.Totusi, listele de parametri din Common Lisp iti dau si moduri mai flexibile de mapare a argumentelor dintr-un apel de functie la parametrii functiei. Pe langa parametrii obligatorii, o functie poate avea parametri optionali. Sau o functie poate avea un singur parametru care este legat de o lista care contine orice argumente in plus. Si, in sfarsit, argumentele pot fi mapate la parametri folosind cuvinte cheie in locul pozitiei din lista. Deci, listele de parametri din Common Lisp sunt o solutie comoda pentru rezolvarea mai multor probleme de programare.
Parametri Optionali
Exista multe functii, ca
verbose-sum
, carora le sunt suficienti parametrii obligatorii, dar nu toate sunt atat de simple. Uneori o functie va avea un parametru care va interesa numai pe anumiti apelatori, poate din cauza ca exista o valoare implicita rezonabila. Un exemplu ar fi o functie care creeaza o structura de date care poate creste dupa necesitati. Deoarece structura de date poate creste nu conteaza - dpdv al corectitudinii - care este marimea initiala. Dar apelatorii care au o idee destul de buna despre cate elemente vor pune in structura de date ar putea sa imbunatateasca performanta specificand o anumita marime initiala. Cei mai multi apelatori, totusi, prefera sa lase codul care implementeaza structura de date sa aleaga o valoare buna in cazul general. In Common Lisp poti sa te potrivesti ambelor tipuri de apelatori folosind un parametru optional; apelatorii carora nu le pasa pot primit o valoare implicita rezonabila, iar ceilalti pot sa transmita o anume valoare.5Pentru a defini o functie cu parametri optionali, dupa numele parametrilor obligatorii, pune simbolul
&optional
urmat de numele parametrilor optionali. Un exemplu simplu arata astfel:(defun foo (a b &optional c d) (list a b c d))Cand este apelata functia, argumentele sunt mai intai legate de parametrii obligatorii. Dupa ce toti parametrii obligatorii au primit valori, daca mai exista argumente ramase, valorile lor sunt atribuite parametrilor optionali. Daca argumentele se termina inaintea parametrilor optionali, acestia primesc valoarea
NIL
. Astfel, functia definita anterior da urmatoarele rezultate: (foo 1 2) ==> (1 2 NIL NIL) (foo 1 2 3) ==> (1 2 3 NIL) (foo 1 2 3 4) ==> (1 2 3 4)Lisp tot verifica daca a fost transmis un numar suficient de argumente functiei - in cazul de fata intre doua si patru, inclusiv - si va semnala o eroare daca functia este apelata cu prea putine sau prea multe.
Desigur, de multe ori o sa vrei o alta valoare decat
NIL
. Poti sa specifici valoarea implicitia inlocuind numele parametrului cu o lista continand un nume si o expresie. Expresia va fi evaluata doar daca apelantul nu transmite suficienti parametri cat sa isi ia o valoare parametrul optional. In general se da o valoare drept expresia respectiva.(defun foo (a &optional (b 10)) (list a b))Aceasta functie cere un argument care va fi legat la parametrul
a
. Al doilea parametru, b
, va prelua ori valoarea celui de-al doilea argument, daca exista unul, sau 10.(foo 1 2) ==> (1 2) (foo 1) ==> (1 10)Uneori s-ar putea sa ai nevoie de mai multa flexibilitate in alegerea valorii implicite. Poate vrei sa o calculezi pe baza celorlalti parametri. Si poti - valoarea implicita se poate referi la parametri care apar mai devreme in lista de parametri. Daca ai scrie o functie care sa returneze o reprezentare a unui dreptunghi si ai vrea sa faci sa fie cat mai usor sa se creeze patrate, ai putea sa folosesti o lista de argumente astfel:
(defun make-rectangle (width &optional (height width)) ...)ceea ce ar face ca parametrul
height
(inaltime) sa ia aceeasi valoare cu parametrul width
(latime) daca nu primeste explicit alta valoare.Ocazional, e util de stiut daca valoarea unui argument optional a fost transmisa de apelant sau este valoarea implicita. In loc sa scrii cod care sa verifice daca valoarea parametrului este cea implicita (care nici macar n-ar functiona cum trebuie, daca apelantul transmite explicit valoarea implicita), poti sa adaugi un alt nume de variabila specificatorului de parametru dupa expresia de valoare implicita. Aceasta variabila va primi valoarea adevarat daca apelantul chiar a transmis un argument pentru parametrul respectiv si
NIL
in caz contrar. Prin conventie, aceste variabile au acelasi nume cu parametrul propriu-zis, urmate de "-supplied-p". De exemplu:(defun foo (a b &optional (c 3 c-supplied-p)) (list a b c c-supplied-p))Rezultatele acestei definitii sunt in genul:
(foo 1 2) ==> (1 2 3 NIL) (foo 1 2 3) ==> (1 2 3 T) (foo 1 2 4) ==> (1 2 4 T)
Parametri Rest
Parametrii optionali sunt foarte buni cand ai parametri discreti pentru care apelantul poate sa doreasca sau nu sa transmita valori. Dar unele functii au nevoie sa preia un numar variabila de argumente. Cateva din functiile interne (built-in) pe care le-ai vazut deja, functioneaza astfel.
FORMAT
are doua argumente obligatorii, fluxul (stream-ul) si string-ul de control. Dar dupa acestea are nevoie de un numar variabil de argumente in functie de cate valori trebuie interpolate in sirul de control. Functia +
primeste un numar variabile de argumente, de asemenea - nu exista nici un motiv sa fie limitata sa adune doar doua numere; va aduna orice numere primite. (Functioneaza chiar si cu zero argumente, returnand 0, identitatea adunarii). Toate apelurile urmatoare sunt legale:(format t "hello, world") (format t "hello, ~a" name) (format t "x: ~d y: ~d" x y) (+) (+ 1) (+ 1 2) (+ 1 2 3)Evident, ai putea sa scrii functii care primesc un numar variabil de argumente pur si simplu specificand ca au multi parametri optionali. Dar ar fi foarte dificil - chiar si numai scrierea listei de parametri ar fi destul de rea, si asta nu implica lucrul cu parametrii in interiorul functiei. Pentru a o face bine, ar trebui sa ai la fel de multe argumente pe cate pot fi transmise legal intr-un apel de functie. Numarul acesta depinde de implementare dar este garantat sa fie macar 50. Si in implementarile curente este intre 4,096 si 536,870,911.6 Bleah. Tipul asta de plictiseala crancena sigur nu este Calea Lisp.
In loc de asta, Lisp permite includerea unui parametru dupa simbolul
&rest
. Daca o functie include un parametru &rest
, orice argumente care nu sunt atribuite parametrilor obligatorii sau optionali sunt stranse intr-o lista care devine valoarea parametrului &rest
. Deci, listele de parametri pentru FORMAT
si +
probabil arata in genul asta:(defun format (stream string &rest values) ...) (defun + (&rest numbers) ...)
Parametri Cuvinte Cheie
Parametrii optionali si rest iti dau destul de multa flexibilitate, dar nici unul n-o sa te ajute prea mult in situatia urmatoare: Sa zicem ca ai o functie care primeste patru parametri optionali. Si sa mai zicem ca in majoritatea locurilor in care este apelata o functie apelantul vrea sa specifice un singur parametru si ca in general fiecare parametru este specificat la fel de mult ca ceilalti.
Apelantii care vor sa transmita o valoare in primul parametru se descurca - ei transmit doar un parametru optional, fara ceilalti trei. Dar toti ceilalti apelanti trebuie sa transmita o valoare pentru unul pana la trei argumente care nu-i intereseaza. Nu este oare chiar problema pe care ar trebui s-o rezolve parametrii optionali?
Sigur ca da. Problema este ca parametrii optionali sunt tot pozitionali - daca apelantul doreste sa transmita o valoare explicita pentru al patrulea parametru, atunci primii trei parametri optionali sunt obligatorii pentru apelantul respectiv. Din fericire, un alt tip de parametri - cuvinte cheie - permit apelantului sa specifice care valori se potrivesc caror parametri.
Pentru a da parametri cuvinte cheie unei functii, se include simbolul
&key
dupa toti parametrii &optional
si &rest
, urmat de orice numar de specificatori de parametri cuvinte cheie, care functioneaza ca specificatorii de parametri optionali. Uite o functie care are numai parametri cuvinte cheie: (defun foo (&key a b c) (list a b c))Cand este apelata aceasta functie, fiecare parametru cuvant cheie este legat la valoarea imediat urmatoare unui parametru cuvant cheie cu acelasi nume. In Capitolul 4 spuneam ca un cuvant cheie este un nume care incepe cu ":" si este definit automat ca o constanta auto-evaluatoare.
Daca un anumit cuvant cheie nu apare in lista de argumente, atunci parametrului corespunzator ii este atribuita valoarea implicita, la fel ca unui parametru optional. Din cauza ca argumentele cuvinte cheie sunt etichetate, pot fi transmise in orice ordine atata vreme cat urmeaza eventualelor argumente obligatorii. De exemplu,
foo
poate fi invocata astfel:(foo) ==> (NIL NIL NIL) (foo :a 1) ==> (1 NIL NIL) (foo :b 1) ==> (NIL 1 NIL) (foo :c 1) ==> (NIL NIL 1) (foo :a 1 :c 3) ==> (1 NIL 3) (foo :a 1 :b 2 :c 3) ==> (1 2 3) (foo :a 1 :c 3 :b 2) ==> (1 2 3)La fel cu parametrii optionali, parametrii cuvinte cheie pot avea valoare implicita si o variabila supplied-p. Atat la parametrii cuvinte cheie cat si la cei optionali, valoarea implicita poate sa se refere la parametri care apar mai devreme in lista de parametri.
(defun foo (&key (a 0) (b 0 b-supplied-p) (c (+ a b))) (list a b c b-supplied-p)) (foo :a 1) ==> (1 0 1 NIL) (foo :b 1) ==> (0 1 1 T) (foo :b 1 :c 4) ==> (0 1 4 T) (foo :a 2 :b 1 :c 4) ==> (2 1 4 T)De asemenea, daca, dintr-un motiv sau altul, vrei ca apelantul sa foloseasca un cuvant cheie diferit de numele parametrului propriu-zis, poti sa inlocuiesti numele parametrului cu alta lista continand cuvantul cheie de folosit la apelarea functiei si numele de folosit pentru parametru. Definitia urmatoare a lui
foo
:(defun foo (&key ((:apple a)) ((:box b) 0) ((:charlie c) 0 c-supplied-p)) (list a b c c-supplied-p))lasa apelantul sa-l foloseasca astfel:
(foo :apple 10 :box 20 :charlie 30) ==> (10 20 30 T)Acest stil e cel mai folositor daca vrei sa decuplezi complet API-ul public al functiei de detaliile interne, de obicei deoarece vrei sa folosesti nume scurte la variabile intern dar cuvinte descriptive in API. Totusi nu este foarte frecvent folosit.
Amestecarea Diferitelor Tipuri de Parametri
E posibil, dar rar, sa se foloseasca toate tipurile de parametri intr-o singura functie. Oricand este folosit mai mult de un tip, declaratia trebuie facuta in ordinea in care am discutat aici despre ele: intai numele parametrilor obligatorii, apoi parametrii optionali, apoi parametrul rest si in final parametrii cuvinte cheie. Totusi, in functiile care folosesc mai multe tipuri de parametri vei combina de obicei parametrii obligatorii cu alt tip sau poate vei combina parametri
&optional
si &rest
. Celelalte doua combinatii, parametri &optional
sau &rest
combinati cu parametri &key
, pot duce la comportamente oarecum surprinzatoare.Combinarea parametrilor
&optional
si &key
duce la rezultate suficient de surprinzatoare cat sa incerci sa o eviti complet. Problema este ca daca un apelant nu transmit evalori pentru toti parametrii optionali, acei parametri necompletati vor consuma cuvintele cheie si valorile intentionate pentru parametrii cuvinte-cheie. De exemplu, aceasta functie amesteca parametri &optional
si &key
:(defun foo (x &optional y &key z) (list x y z))Daca este apelata asa, functioneaza bine:
(foo 1 2 :z 3) ==> (1 2 3)Si asa este bine:
(foo 1) ==> (1 nil nil)Dar asa va rezulta o eroare:
(foo 1 :z 3) ==> ERRORAsta e din cauza cuvantului cheie
:z
care este preluat ca valoare pentru a completa parametrul optional y
, lasand doar argumentul 3 sa fie procesat. In acel punct, Lisp se asteapta ori la o pereche cuvant cheie/valoare ori la nimic si se va plange. Poate mai rau, daca functia avea doi parametri &optional
, acest apel ar fi rezultat in legarea valorilor :z
si 3 de cei doi parametri &optional
, z
, parametrul &key
primind valoarea implicita NIL
fara nici o indicatie cum ca s-ar fi intamplat ceva in neregula.In general, daca ai nevoie sa scrii o functie care foloseste atat parametri
&optional
cat si &key
, probabil ar trebui sa-i schimbi pe toti in parametri &key
- sunt mai flexibili si poti oricand sa adaugi parametri cuvinte cheie noi fara sa deranjezi apelantii existenti ai functiei. De asemenea poti sa elimini parametri cuvinte cheie, daca nu sunt folositi.7 Ingeneral, folosirea parametrilor cuvinte cheie ajuta la scrierea de cod mult mai usor de mentinut si dezvoltat - daca ai nevoie sa adaugi un comportament intr-o functie care are nevoie de parametri noi, poti sa adaugi parametri cuvinte cheie fara sa trebuiasca sa atingi codul existent care apeleaza functia.
Poti sa combini in siguranta parametri
&rest
si &key
, dar comportamentul ar putea fi un pic surprinzator initial. In mod normal prezenta lui &rest
sau &key
intr-o lista de parametri face ca toate valorile dintre ramase dupa atribuirea parametrilor obligatorii si cei &optional
sa fie procesate intr-un anume fel - ori adunate intr-o lista pentru un parametru &rest
ori atribuite parametrilor &key
potriviti pe baza cuvintelor cheie. Daca atat &rest
cat si &key
apar in lista, atunci se intampla amandoua lucrurile - toate valorile care raman, inclusiv cuvintele cheie, sunt adunate intr-o lista care e legata in parametrul &rest
si valorile corespunzatoare sunt legate si la parametrii &key
. Deci, daca avem functia: (defun foo (&rest rest &key a b c) (list rest a b c))rezultatul va fi:
(foo :a 1 :b 2 :c 3) ==> ((:A 1 :B 2 :C 3) 1 2 3)
Valorile Returnate din Functii
Toate functiile pe care le-ai scris pana acum au returnat valoarea ultimei expresii evaluate ca valoarea functiei, comportament implicit. Este cel mai des intalnit mod de a returna valori dintr-o functie.
Totusi, uneori este comod sa returnezi din mijlocul unei functii, de exemplu cand vrei sa iesi din niste constructii de control imbricate. In asemenea cazuri poti sa folosesti operatorul special
RETURN-FROM
pentru a returna imediat din functie orice valoare.Vei vedea in Capitolul 20 ca
RETURN-FROM
nu este legat de functii in mod special ci este folosit pentru a returna dintr-un bloc de cod definit cu operatorul special BLOCK
. DEFUN
impacheteaza automat tot corpul functiei intr-un bloc cu acelasi nume ca functia. Deci, evaluarea unui RETURN-FROM
denumit ca functia din care face parte si valoarea pe care o vrei returnata va face ca functia sa se termine imediat returnand valoarea respectiva. RETURN-FROM
este un operator special al carui prim "argument" este numele blocului din care trebuie sa returneze. Acest nume nu este evaluat si de aceea nu este citat.Functia urmatoare foloseste bucle imbricate pentru a gasi prima pereche de numere, fiecare mai mic decat 10, al caror produs este mai mare decat argumentul, si foloseste
RETURN-FROM
pentru a returna perechea de indata ce o gaseste:(defun foo (n) (dotimes (i 10) (dotimes (j 10) (when (> (* i j) n) (return-from foo (list i j))))))De acord, faptul ca trebuie specificat numele functiei din care iesi e un pic incomod - de exemplu, daca schimbi numele functiei va trebui sa schimbi si numele folosit in
RETURN-FROM
.8 Dar RETURN-FROM
explicit este folosit mult mai rar in Lisp decat declaratiile return
in limbajele derivate din C, deoarece toate expresiile Lisp, inclusiv constructiile de control gen bucle si conditii, sunt evaluate la o valoare. Deci nu este chiar asa problema in practica. Functii ca Date, a.k.a. Functions de Ordin Mai Inalt
In general vei folosi functiile apelandu-le dupa nume, dar exista si un numar de situatii in care e bine sa poti trata functiile ca si date. De exemplu, daca ai putea transmite o functie ca argument unei alte functii, ati putea scrie o functie de sortare generala, permitand apelantului sa foloseasca ce functie vrea el pentru compararea a oricare doua elemente. Atunci acelasi algoritm poate fi folosit cu mai multe functii de comparare. Similar, apelurile inapoi (callbacks) si hooks depind de posibilitatea de a pastra referinte la cod pentru a-l putea rula mai tarziu. Deoarece functiile sunt deja modul standard de a abstractiza bucati de cod, are sens sa permitem functiilor sa fie tratate ca date.9
In Lisp, functiile sunt doar un alt tip de obiect. Cand definesti o functie cu
DEFUN
, de fapt faci doua lucruri: creezi un obiect nou de tip functie si ii dai un nume. E posibil si sa folosesti expresii LAMBDA
pentru a crea functii fara a le da nume, dupa cum ai v azut in Capitolul 3. Reprezentarea actuala a unui obiect functie, indiferent ca are sau nu nume, este opaca - intr-un Lisp care compileaza in cod nativ probabil consta in mare parte din cod masina. Singurele lucruri pe care trebuie sa le stii sunt cum sa il obtii si cum sa il invoci odata ce l-ai obtinut.Operatorul special
FUNCTION
da un mecanism pentru a obtine un obiect functie. Primeste un singur argument si returneaza functia cu numele respectiv. Numele nu este citat. Astfel, daca ai definit o functie foo
:CL-USER> (defun foo (x) (* 2 x)) FOOpoti sa obtii obiectul functie astfel:10
CL-USER> (function foo) #<Interpreted Function FOO>De fapt, deja ai folosit
FUNCTION
, dar deghizat. Sintaxa #'
, pe care ai folosit-o in Capitolul 3, e doar o alta forma de a scrie FUNCTION
, la fel cum '
este alt mod de a scrie QUOTE
.11 Deci, poti sa obtii obiectul functie pentru foo
si astfel: CL-USER> #'foo #<Interpreted Function FOO>Odata ce ai obtinut obiectul functie, nu poti sa faci decat un lucru cu el - sa-l invoci. Common Lisp are doua functii pentru a invoca o functie printr-un obiect functie:
FUNCALL
si APPLY
.12 Diferenta intre ele este in modul in care isi obtin argumentele pe care le transmit functiei. FUNCALL
merge folosit atunci cand stii numarul de argumente pe care il vei transmite functiei la momentul scrierii codului. Primul argument la FUNCALL
este obiectul functie care trebuie invocat, iar restul sunt argumentele care sunt transmise functiei. Astfel, urmatoarele doua expresii sunt echivalente:(foo 1 2 3) === (funcall #'foo 1 2 3)Totusi, nu prea are rost sa folosesti
FUNCALL
pentru a apela o functie pe care o cunosti la momentul scrierii codului. De fapt, cele doua expresii anterioare probabil se compileaza in aceleasi instructiuni-masina.Urmatoarea functie demonstreaza un uz mai potrivit pentru
FUNCALL
. Accepta un obiect functie ca argument si afiseaza o simpla histograma ASCII a valorilor returnate de functie atunci cand este invocata cu valorile dintre min
si max
, pasind cu step
.(defun plot (fn min max step) (loop for i from min to max by step do (loop repeat (funcall fn i) do (format t "*")) (format t "~%")))Expresia
FUNCALL
calculeaza valoarea functiei pentru fiecare valoare a lui i
. Bucla interioara LOOP
foloseste acea valoare calculata pentru a determina de cate ori sa afiseze un asterisc la iesirea standard.De observat ca nu folosesti
FUNCTION
sau #'
pentru a acces la valoarea ca functie a lui fn
; dimpotriva, o vrei interpretata ca variabila deoarece valoarea variabilei va fi obiectul functie. Poti sa chemi plot
cu orice functie care primeste un singur argument numeric, cum ar fi functia interna EXP
care returneaza valoarea lui e ridicata la puterea argumentului sau. CL-USER> (plot #'exp 0 4 1/2) * * ** **** ******* ************ ******************** ********************************* ****************************************************** NIL
FUNCALL
, totusi, nu te ajuta atunci cand lista de argumente este cunoscuta doar la momentul rularii. De exemplu, ca sa mai ramanem un pic la functia plot
, sa presupunem ca ai obtinut o lista continand un obiect functie, o valoare minima si una maxima si o valoare de pas. Cu alte cuvinte, lista contine valorile pe care vrei sa le transmiti ca argumente lui plot
. Sa presupunem ca aceasta lista este in variabila plot-data
. Ai putea sa invoci plot
pe valorile din acea lista astfel: (plot (first plot-data) (second plot-data) (third plot-data) (fourth plot-data))Asta functioneaza, dar e cam enervant sa trebuiasca despachetate explicit argumentele doar ca sa le trimiti lui
plot
.Aici intervine
APPLY
. La fel cu FUNCALL
, primul argument catre APPLY
este un obiect functie. Dar dupa obiectul functie, in loc de argumente individuale, trebuie sa urmeze o lista. Apoi aplica functia valorilor din acea lista. Asta iti permite sa scrii astfel:(apply #'plot plot-data)Ca un ajutor in plus,
APPLY
accepta si argumente "libere" atata timp cat ultimul argument e o lista. Deci, daca plot-data
continea doar valorile min, max si step, tot poti sa folosesti APPLY
astfel pentru a afisa functia EXP
pe intervalul respectiv:(apply #'plot #'exp plot-data)Pentru
APPLY
nu conteaza daca functia aplicata primeste argumente &optional
, &rest
sau &key
- lista de argumente produsa prin combinarea oricaror argumente libere cu lista finala trebuie sa fie lista de argumente legala pentru functie cu suficiente argumente pentru toti parametrii obligatorii si doar pentru parametrii cuvinte cheie potriviti. Functii Anonime
Odata ce incepi sa scrii, sau doar sa folosesti functii care accepta alte functii ca argumente, vei descoperi ca uneori e enervant sa trebuiasca sa definesti si sa numesti o intreaga functie separata care e folosita doar intr-un loc, mai ales cand nu o chemi niciodata pe nume.
Cand pare prea multa munca sa definesti o functie noua cu
DEFUN
, poti sa creezi o functie "anonima" folosind o expresie LAMBDA
. Dupa cum am discutat in Capitolul 3, o expresie LAMBDA
arata astfel:(lambda (parametri) corp)Un mod de a te gandi la expresii
LAMBDA
este ca la un tip special de functie in care numele insusi descrie direct ce face functia. Aceasta explica de ce poti folosi o expresie LAMBDA
in locul unui nume de functie cu #'
.(funcall #'(lambda (x y) (+ x y)) 2 3) ==> 5Poti sa folosesti
LAMBDA
chiar si ca "numele" unei functii intr-un apel de functie. Daca ai vrea, ai putea sa scrii expresia FUNCALL
anterioara mai concis. ((lambda (x y) (+ x y)) 2 3) ==> 5Dar asta nu se intampla aproape niciodata; doar ca merita mentionat ca este legal pentru a sublinia ca expresiile
LAMBDA
pot fi folosite oriunde ar fi folosit numele unei functii.13Functiile anonime pot fi utile cand ai nevoie sa transmiti o functie ca argument unei alte functii si functia pe care vrei sa o transmiti e destul de simpla pentru a o exprima adhoc (inline). De exemplu, sa zicem ca ai vrea sa afisezi functia 2x. Ai putea defini urmatoarea functie:
(defun double (x) (* 2 x))pe care sa o transmiti apoi lui
plot
.CL-USER> (plot #'double 0 10 1) ** **** ****** ******** ********** ************ ************** **************** ****************** ******************** NILDar e mai simplu, si discutabil mai clar, sa scrii asa:
CL-USER> (plot #'(lambda (x) (* 2 x)) 0 10 1) ** **** ****** ******** ********** ************ ************** **************** ****************** ******************** NILCelalalt mod important de folosire a expresiilor
LAMBDA
este in crearea inchiderilor, functii care captureaza o parte din mediul in care sunt create. Ai folosit inchideri un pic in Capitolul 3, dar detaliile despre cum functioneaza ele si la ce sunt folosite tin mai degraba de modul de functionare a variabilelor decat al functiilor, asa ca voi lasa discutia pentru capitolul urmator.1In ciuda importantei functiilor in Common Lisp, descrierea de limbaj functional nu este foarte precisa. E adevarat ca unele din capabilitatile lui Common Lisp, cum ar fi functiile de manipulare a listelor, sunt proiectate pentru a fi folosite intr-un stil forma-corp* si ca Lisp are un loc insemnat in istoria programarii functionale - McCarthy a introdus multe idei care acum sunt considerate importante in programarea functionala - dar Common Lisp a fost proiectat intentionat in asa fel incat sa suporte multe stiluri diferite de programare. In familia Lisp, Scheme este cel mai apropiat lucru de un limbaj functional "pur", si chiar el are cateva capabilitati care il descalifica de la puritatea absoluta prin comparatie cu limbaje cum ar fi Haskell and ML.
2Ma rog, aproape orice simbol. Nu este definit ce se intampla daca folosesti oricare din numele definite in standardul limbajului ca nume al uneia din functiile proprii. Dar, dupa cum vei vedea in Capitolul 21, sistemul de pachete Lisp iti permita sa creezi numele in spatii de nume (namespaces) diferite, deci asta nu e chiar o problema.
3Listele de parametri mai sunt numite si liste lambda datorita relatiei istorice dintre notiunea Lisp de functii si calculul lambda.
4De exemplu, urmatoarul rand:
5In limbajele care nu suporta parametri optionali direct, programatorii gasesc de obicei moduri de a-i simula. O tehnica este sa foloseasca anumite valori "fara valoare" pe care apelantul le poate transmite pentru a indica folosirea valorii implicite la un anumit parametru. In C, de exemplu, este obisnuit sa se foloseasca
6Constanta
7Exista patru functii standard care primesc atat argumente
8Alt macro,
9Lisp nu este singurul limbaj care trateaza functiile ca si date, desigur. C folosestei pointeri la functii, Perl foloseste referinte la subrutine, Python foloseste o abordare similara Lisp si C# introduce delegatii, la baza pointeri cu tip catre functii (essentially typed function pointers), ca imbunatatire fata de mecanismele de reflectie si clasele anonime destul de incomode din Java.
10Reprezentarea exacta in scris a unui obiect functie difera de la o implementare la alta.
11Cel mai bun mod de a ne gandi la
12Mai exista un al treilea, operatorul special
13In Common Lisp e posibil sa se foloseasca o expresie
Dar daca inca ar fi adevarat, atunci
2Ma rog, aproape orice simbol. Nu este definit ce se intampla daca folosesti oricare din numele definite in standardul limbajului ca nume al uneia din functiile proprii. Dar, dupa cum vei vedea in Capitolul 21, sistemul de pachete Lisp iti permita sa creezi numele in spatii de nume (namespaces) diferite, deci asta nu e chiar o problema.
3Listele de parametri mai sunt numite si liste lambda datorita relatiei istorice dintre notiunea Lisp de functii si calculul lambda.
4De exemplu, urmatoarul rand:
(documentation 'foo 'function)returneaza string-ul documentar pentru functia
foo
. Totusi, trebuie sa observi ca intentia este ca stringul documentar sa fie citit de oameni, nu accesat programatic. O implementare Lisp nu trebuie sa le pastreze si are voie sa le indeparteze in orice moment, asadar programele portabile nu ar trebui sa se bazeze pe prezenta lor. In unele implementari este necesar sa se seteze o variabila specifica pentru a salva stringurile documentare.5In limbajele care nu suporta parametri optionali direct, programatorii gasesc de obicei moduri de a-i simula. O tehnica este sa foloseasca anumite valori "fara valoare" pe care apelantul le poate transmite pentru a indica folosirea valorii implicite la un anumit parametru. In C, de exemplu, este obisnuit sa se foloseasca
NULL
drept valoarea speciala. Totusi, un asemenea protocol intre functie si apelantii sai este ad hoc - in unele functii sau pentru unele argumente NULL
ar putea fi acea valoare speciala in timp ce pentru alte functii ar putea sa fiee -1 sau vreo alta constanta #definita
.6Constanta
CALL-ARGUMENTS-LIMIT
indica valoarea specifica implementarii respective.7Exista patru functii standard care primesc atat argumente
&optional
cat si &key
- READ-FROM-STRING
, PARSE-NAMESTRING
, WRITE-LINE
si WRITE-STRING
. Au fost lasate asa in timpul standardiyarii pentru a pastra compatibilitate cu dialectele anterioare Lisp. READ-FROM-STRING
tinde sa fie cea care pacaleste noii programatori Lisp cel mai frecvent - un apel in genul asta (read-from-string s :start 10)
pare sa ignore argumentul cuvant cheie :start
, incepand citirea de la indexul 0 in loc de 10. Asta se intampla din cauza ca READ-FROM-STRING
are si doi parametri &optional
care preiau argumentele :start
si 10.8Alt macro,
RETURN
, nu are nevoie de nume. Dar nu poate fi folosit in loc de RETURN-FROM
pentru a evita necesitatea specificarii numelui functiei; e alt mod (syntactic sugar) de a returna dintr-un bloc denumit NIL
. Voi acoperi acest subiect, impreuna cu detalii despre BLOCK
si RETURN-FROM
, in Capitolul 20.9Lisp nu este singurul limbaj care trateaza functiile ca si date, desigur. C folosestei pointeri la functii, Perl foloseste referinte la subrutine, Python foloseste o abordare similara Lisp si C# introduce delegatii, la baza pointeri cu tip catre functii (essentially typed function pointers), ca imbunatatire fata de mecanismele de reflectie si clasele anonime destul de incomode din Java.
10Reprezentarea exacta in scris a unui obiect functie difera de la o implementare la alta.
11Cel mai bun mod de a ne gandi la
FUNCTION
este ca un tip special de citare. QUOTE
area unui simbol il face sa nu fie evaluat, rezultand in simbolul propriu-zis in loc de valoarea variabilei numite de simbol. La fel, FUNCTION
ocoleste regula normala de evaluare dar, in loc sa faca simbolul neevaluat, face ca acesta sa fie evaluat ca numele unei functii, ca si cand ar fi fost folosit direct ca numele functiei intr-o expresie care apeleaza o functie.12Mai exista un al treilea, operatorul special
MULTIPLE-VALUE-CALL
, dar voi lasa asta pentru cand voi discuta despre expresii care intorc valori multiple in Capitolul 20.13In Common Lisp e posibil sa se foloseasca o expresie
LAMBDA
ca argument pentru FUNCALL
(sau vreo alta functie care primeste functii ca argument cum ar fi SORT
sau MAPCAR
) fara #'
inainte, astfel:(funcall (lambda (x y) (+ x y)) 2 3)Este legal si echivalent cu versiunea cu
#'
dar dintr-un motiv special. In mod istoric expresiile LAMBDA
nu erau expresii care puteau fi evaluate. Adica LAMBDA
nu era numele unei functii, macro sau operator special. Mai degraba, o lista care incepea cu simbolul LAMBDA
era o constructie sintactica speciala pe care Lisp o recunostea ca pe un fel de nume de functie.Dar daca inca ar fi adevarat, atunci
(funcall (lambda (...) ...))
ar fi ilegal deoarece FUNCALL
este functie si regula normala de evaluare pentru apelurile de functii ar cere sa fie evaluata expresia LAMBDA
. Dar, destul de tarziu in procesul de standardizare ANSI, pentru a putea implementa ISLISP, un alt dialect Lisp care era in curs de standardizare pe atunci, strict ca strat de compatibilitate la nivel de utilizator peste Common Lisp, a fost definit un macro LAMBDA
care expandeaza intr-un apel catre un FUNCTION
impachetand o expresie LAMBDA
. Cu alte cuvinte, expresia LAMBDA
:(lambda () 42)este expandata astfel atunci cand apare intr-un context in care e evaluata:
(function (lambda () 42)) ; sau #'(lambda () 42)De aceea folosirea ei intr-o pozitie de valoare, ca argument la
FUNCALL
, este legala. Cu alte cuvinte, e un alt mod de scriere. Cei mai multi oameni ori folosesc totdeauna #'
inainte de expresiile LAMBDA
in pozitiile de valori ori niciodata. In aceasta carte folosesc totdeauna #'
.Sus
Functiile fac treaba in Lisp; macrourile genereaza cod, dar acel cod ajunge tot sa apeleze functii.
Functiile se definesc cu macroul DEFUN:
(defun nume (parametru*) "Sir de caractere optional, pentru documentare" forma-corp*)Exemplu:
(defun verbose-sum (x y) "Insumeaza oricare doua numere dupa ce afiseaza un mesaj." (format t "Insumez ~d si ~d.~%" x y) (+ x y))Cam orice simbol poate fi folosit ca nume de functie. Pe langa caractere alfanumerice si cratime sunt permise si alte caractere, cum ar fi ">". Pot exista in acelasi timp o functie cu numele ceva si o variabila cu numele ceva, motiv pentru care Common Lisp este denumit Lisp-2 - Lisp cu doua spatii de nume separate pentru variabile si functii.
Corpul functiei poate contine orice numar de forme Lisp, care vor fi evaluate in ordine atunci cand functia este apelata; valoarea ultimei expresii este returnata ca valoarea functiei. Se poate iesi din functie si cu operatorul special RETURN-FROM.
Parametrii functiei sunt variabilele care vor primi argumentele transmise functiei atunci cand este apelata. Acestia pot fi obligatorii, optionali, denumiti cu cuvinte cheie sau parametri care pot contine o lista de valori de lungime necunoscuta.
Parametrii obligatorii sunt scrisi ca simple nume in lista de parametri a functiei, pentru fiecare dintre ei trebuind sa fie transmisa cate o valoare si numai una la momentul apelului functiei.
Exemplu:
(defun add-1 (x) (+ x 1))Parametrii optionali primesc valori cand functia este apelata doar daca toti parametrii obligatorii au primit valori; ei se definesc in felul urmator, adaugandu-i dupa simbolul &optional care urmeaza parametrii obligatorii:
(defun add-some (x &optional some) (+ x (or some 1)))Aici
x
este obligatoriu, iar some
este definit ca fiind optional. Functia poate fi apelata astfel:(add-some 10) ==> 11 (add-some 10 20) ==> 30Parametrii optionali pot primi valoare implicita, care le este atribuita atunci cand functia nu a primit argument pentru ei, si pot sa si stie daca valoarea pe care o au este cea implicita sau a fost transmisa de apelant:
(defun foo (a b &optional (c 3 c-supplied-p)) (list a b c c-supplied-p))Parametrii rest sunt folositori pentru functii care iau numar variabil de argumente, cum ar fi +. Modul de definire este includerea unui parametru dupa simbolul &rest, la sfarsitul listei de parametri a functiei. Exemplu:
(defun + (&rest numbers) ...) (+) (+ 1) (+ 1 2 3)Un alt tip de parametri sunt cei prefixati de cuvinte cheie. Avantajul dat de ei este ca se poate modifica ordinea atribuirii lor in momentul apelului functiei.
(defun add-2-numbers (&key n1 n2) (+ n1 n2)) (add-2-numbers :n2 20 :n1 1) ==& 21Pentru a vedea cum poti amesteca mai multe tipuri de parametri intr-o functie, citeste te rog capitolul.
O functie poate sa returneze una sau mai multe valori; pentru a returna mai multe valori foloseste VALUES:
(defun bracket (x) (values (+ x 1) x (- x 1))) (bracket 3) ==> 4 (bracket 3) ==> 3 (bracket 3) ==> 2
Functiile sunt tip de date obisnuit in Lisp, adica pot fi transmise ca parametri, pot fi returnate din alte functii, pot fi stocate in variabile.
Poti apela functii stocate in vreo variabila folosind functiile FUNCALL si APPLY. Prima ia ca parametri functia si parametrii apelului, a doua functia si o lista continand parametrii apelului. Functia poti s-o obtii dintr-o variabila folosind operatorul special FUNCTION.
Poti sa si construiesti functii anonime, pentru cazuri in care trebuie sa folosesti o functie pe care nu are rost sa o definesti separat, de exemplu pentru ca nu ai nevoie de ea in alta parte in program. Asta se face cu lambda:
(mapcar #'(lambda (x) (+ x 1)) (list 1 2 3)) ==> (1 2 3)
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.