A programa funcțional este a programa. Consensul programatorilor, mai ales al celor care folosesc programarea funcțională, este că programarea funcțională este viitorul. Ei ne zic că este mai ușor de înțeles codul, pentru că nu trebuie să ținem cont de starea sistemului. Codul e mai succint, pentru că refolosim zeci și poate sute de funcții predefinite care au fost scrise deja și ne permit să ne concentrăm direct pe implementare.
Totuși programarea funcțională reprezintă doar o mică parte a pieței. Ea poate fi mai dificilă de înțeles, decât cea imperativă sau orientată pe obiecte. În aceste paradigme un programator poate învăța conceptele pe parcurs, însă când vine vorba de programarea funcțională, un programator se descurcă doar dacă cunoaște deja majoritatea funcțiilor și conceptelor folosite des.
Cu toate acestea, toate limbajele moderne încorporează concepte luate din programarea funcțională, precum pattern match, funcții lambda și funcții higher order. În acest articol nu voi încerca să promovez programarea funcțională în detrimentul altor paradigme, ci voi prezenta concepte de bază care se pot aplica oriunde, folosind aproape orice limbaj modern.
În paradigma OO considerăm un program ca fiind un număr de obiecte care interacționează cu alte obiecte folosind interfețele obiectelor. Fiecare obiect are niște date și niște metode asociate. Programarea funcțională, putem spune, este orientată pe funcții.
Metodele din limbajele OO nu sunt neapărat funcții, deși uneori se pot trata ca funcții. O metodă poate fi o procedură, o funcție sau o combinație de metodă și funcție. O procedură ia niște parametri, și modifică ori parametri, ori alte variabile din sistem. Funcția produce o valoare nouă folosind doar parametri de intrare. E important de menționat faptul că funcția nu modifică parametri de intrare. Tot ce modifică parametrii de intrare nu sunt funcții, sunt proceduri, chiar dacă returnează valori noi. Din această cauză cele mai multe metode sunt proceduri.
În viziunea programării funcționale, tot programul este considerat o singură funcție. Funcția este compusă din mai multe funcții mai mici și tot așa. O deosebire între OO și PF este că în OO se zice "Functions in the small, objects in the large", adică se folosesc obiecte pentru logica abstractă, dar se scriu metode pentru logica concretă. În schimb în PF, fiecare funcție este alcătuită dintr-o altă funcție, până când ajungem la o valoare "Functions in the small, functions in the large".
În cazul în care nu sunteți siguri dacă o bucată de cod este sau nu o funcție, puneți-vă următoare întrebare: se poate înlocui bucata de cod cu un tabel? Dacă răspunsul este da, atunci aveți o funcție în față. Dacă nu, vă uitați la o procedură.
Când se vorbește despre polimorfism, de obicei se face în cadrul programării OO. Există însă polimorfism și în PF. În PF polimorfismul este obținut prin (surpriză) funcții.
Cum adăugăm o funcționalitate a unui număr de clase? Prin folosirea unei interfețe comune cu unul sau mai multe metode, forțând fiecare clasă care implementează interfața, să implementeze metodele din interfață. În programarea funcțională acest lucru se poate replica prin a face o funcție care primește un obiect și returnează o valoare bazată pe obiectul primit.
Mai jos în stânga, am definit o interfață Zi care are o singură metodă nume. Apoi am extins un număr de clase care implementează interfața Zi. În dreapta, am definit o structură echivalentă în programarea funcțională. Aceste exemple sunt scrise în Scala, un limbaj care combină programarea funcțională cu programarea OO.
Putem observa că în primul caz (stânga) sunt grupate metodele care acționează peste un tip de date, iar în cazul doi (dreapta), sunt grupate obiectele care au o anumită proprietate.
Un alt fel de a vedea lucrurile, este că în primul caz, uitându-ne la un obiect putem vedea toate metodele definite pentru acel obiect, însă e mai dificil de văzut toate obiectele care au un anumit comportament.
În cazul doi, uitându-ne la o funcție putem vedea toate obiectele care au definit acel comportament, dar e mai greu să aflăm toate comportamentele pentru un anumit obiect.
Ambele variante sunt valide, cu avantaje și dezavantaje. Totuși, un argument pentru varianta funcțională ar fi că de multe ori când facem debug sau implementăm ceva, e mai folositor să vedem obiectele grupate în funcții, pentru că ne concentrăm pe funcționalitate, nu pe un anumit obiect.
Tony Hoare, informaticianul care a inventat quicksortul printre altele, și-a cerut scuze în 2009 pentru o greșeală imensă introdusă de el în informatică: "I call it my billion-dollar mistake. It was the invention of the null reference in 1965.".
Cum putem scăpa de nulluri? Ușor: Nu returnați nulluri, sub nici o circumstanță. Din nefericire, s-au scris foarte multe sisteme care returnează nulluri și se așteaptă la nulluri în argumente. Pentru cine e obișnuit cu acest mod de programare, s-ar putea să îi fie greu să își imagineze o modalitate diferită de a programa. Însă ea există, și nu e relegată doar la programarea funcțională. Soluția de a scăpa de nulluri este una simplă dar poate nu e familiară: folosirea Optionalurilor.
Un Optional
este un tip care poate conține sau nu conține o valoare, oarecum similar cu o referință care poate fi null sau o valoare existentă. Dacă aderăm la programarea fără nulluri, atunci Optional
ne permite să controlăm mai bine efectele erorilor și lipselor. Aceasta e posibil datorită faptului că există multe funcții existente scrise pentru obiectele de tip Optional
.
În exemplul de mai sus am folosit unele din acele funcții pentru a elimina toate verificările, folosind doar două tipuri de funcții mapN
și map
. mapN
este echivalent cu primul if
, care verifică dacă unul dintre variabile este lipsă. Dacă o variabilă lipsește, se returnează lipsa. În cazul în care ambele variabile sunt prezente, parcurgem lista cu un map
, pe care se creează o nouă listă aplicând funcția lambda. Al doilea map verifică dacă elementul curent din b există. Dacă da, returnează valoarea lui adunată cu a, în caz contrar rămâne o valoare lipsă.
Aceste funcții există în toate limbajele funcționale, dar se pot implementa ușor și în alte limbaje precum Java, C# sau C++. Codul care nu verifică nulluri tot timpul e mai succint, și dacă e citit de cineva care cunoaște funcțiile folosite, e mai transparent.
După cum am văzut în exemplul de mai sus, lista și Option
are o metodă map
. Dacă ne gândim la efectul mapului
pe Option
, se verifică dacă există sau nu o valoare, iar dacă ea există, este transformată în ceva mod. Dacă ne gândim la efectul aplicat unei liste, transformăm toate valorile listei într-un anumit fel.
Putem lua un pas în spate și ajungem la concluzia că lista și Optional sunt tipuri de date similare. Acest lucru devine clar, dacă ne gândim că o listă poate fi goală. Dacă lista este goală, nu se schimbă nici un element. Dacă Optional
e gol, nici atunci. Dacă lista conține valori, ele sunt modificate, similar cu Optional
. (E important să precizăm că de fapt nu se schimbă valorile în listă, ci se creează o altă listă cu noile valori. Acest lucru se aplică și la Optional
). Vom denumi aceste tipuri de date care au funcția map
definită, containere. În programarea funcțională numele folosit este Functor, dar nu este un nume tocmai intuitiv sau prietenos. Exemple de containere sunt liste, optionaluri, arbori de orice fel, procese asincrone (Future / Promise), tipuri care conțin erori, etc.
Unele containere sunt mai surprinzătoare decât altele, dar este cert că se poate implementa un map pentru toate aceste obiecte.
Toate conceptele sunt diferite într-un anumit sens, dar sunt similare într-un alt sens. A vedea similaritățile este la fel de important, chiar mai important decât să ne concentrăm pe diferențe.
În acest articol, scopul a fost evidențierea unor similarități. Acest proces de a face legături între concepte este unul care poate avea beneficii în orice paradigmă sau limbaj de programare .