TSM - Geometria codului - despre ce e programarea funcțională

Florin Bunău - Senior Software Engineer @ ComplyAdvantage


Oamenii au practicat agricultura și creșterea animalelor cu mult înainte să înțeleagă biologia. Au început cu unelte simple, acumulând cunoștințe prin experimentare practică, cunoștințe care au fost transmise generațiilor următoare prin bucățele de folclor. OOP, GRASP, SOLID, DRY, YAGNI, KISS și șabloane de design. Toate aceste povești pe care ni le spunem unii altora și viitoarelor generații stând la focul revizuirii de cod, sunt similare miturilor agricole de fertilitate transmise înainte de fundamentarea unei ramuri științifice.

Gândiți-va la principiile care vă ghidează în dezvoltarea software, toate au un lucru în comun: sunt metode de a controla structura, metode de a îmblânzi roiul digital în beneficiul nostru.

Mintea umana este limitată, memoria de termen scurt poate reține doar între 5 și 9 obiecte în același timp. Pactul pe care l-am făcut cu evoluția, a fost să dăm la schimb memoria de termen scurt pe capacitatea de a face planuri pe termen lung. Dacă înainte oamenii au evoluat orientându-se după forma spațiului ce îi înconjoară, acum programatorii navighează mii de linii prin forma liniilor de cod și a acoladelor împrăștiate. Arătând spre vârfurile înălțate ale claselor și văile metodelor, juniorii sunt îndrumați spre destinația lor, de către bătrânii ce au mai bătut potecile proiectului.

Avem nevoie de structură pentru a înțelege lumea, aceasta fiind în aparență structurată.

Moarte din o mie de tăieturi

Toți programatorii au simțit la un moment dat că software-ul este viu. Are personalitate, are vicii, te poate surprinde, câteodată chiar pare să aibă o conștiință proprie și încearcă să se lupte cu tine.

Sunt momente când nu știi ce gândește, iar acea minte trebuie disecată, pentru a privi în profunzimea gândurilor, în motivația din spatele acțiunilor, denumite popular sub termenii de "depanare" sau "analiza jurnalului".

Software-ul e ca o pădure, o herghelie de cai, un roi de albine sau un stol de păsări. Roiul nu e o ființă vie, dar poate să manifeste proprietăți similare unei ființe vii, numite pe scurt "comportament emergent"

Comportamentul emergent e definit ca ceva ce nu rezultă din părți individuale ci din relația dintre părți. E ceva ce se dezvoltă din aranjamentul și interacțiunea părților. O configurație specifică a unui aranjament o mai numim și "structură".

E ingredientul secret care face un sistem mai mare decât suma părților sale și îl putem regăsi peste tot unde privim, în sisteme fizice, biologice, sociale, software, etc.

Preaslăvirea λ

Iată țelul și miezul programării funcționale: studiul și înțelegerea softului prin prisma unei structuri formale.

Această ramură neînțeleasă, temută și ignorată a ingineriei software, atrage cei mai dedicați și mai ciudați oameni care deseori formează grupuri sectante care întorc spatele maselor vulgare ignorante față de singurul adevăr universal cunoscut cu certitudine de oameni: calculul lambda. (pamflet)

Un asemenea comportament e normal într-un domeniu atât de tânăr ca ingineria software, alte domenii având începuturi similare. Matematica a trecut prin faza sa de "geometrie sacră" când pitagoricienii entuziasmați de frumusețea triunghiului cu unghi drept și a relației dintre unghiurile sale cât și de descoperirea fundamentelor matematice ale muzicii, au format o religie proprie la centrul căreia se afla o comunitate închisă și izolată de lumea exterioară, dedicată unui scop unic de a cerceta și a se bucura de tainele universului.

Din fericire, în ultima vreme, s-a produs o renaștere, o redescoperire a acestui stil de programare ocult puțin practicat. Lucruri ca : "preferința compoziției fată de moștenire", "preferința structurilor de date imutabile", "generice", "Java suportă funcții lambda" sunt mici strecurări ale PF în universul conservator al programării imperative.

În a doua jumătate a acestui articol, voi încerca să arăt ce este programarea funcțională din punct de vedere tehnic, și voi încerca să înlătur falsa imagine a unui cult.

Ce este programarea funcțională?

Să începem cu un exemplu simplu, dintr-o carte pe care o recomand:

def buyCoffee(cc: CreditCard): Coffee = {    
    val cup = new Coffee()    
    cc.charge(cup.price)    
    cup    
  }

O bucată aparent inofensivă de cod, dar care conține ceea ce programarea funcțională numește "efect-secundar". Efectul secundar aici e apelul către cartea de credit, care va încasa prețul unei cafele, scăzând soldul disponibil în cont.

Termenul de secundar e motivat de incapacitatea de a deduce din semnătura funcției ce acțiuni va realiza funcția. Această relație dintre crearea cafelei și transferul banilor de pe credit card nu este formal menționată. Ea există doar ca structură implicită la rulare și nu face parte din structura care poate fi manipulată în construcția softului. Din același motiv nu poate fi folosită într-un raționament efectuat asupra corectitudinii construcției fără a avea o înțelegere perfectă a întregii implementări. Semnătura doar indică necesitatea unei "cărți de credit" pentru a putea produce o cafea, și omite să indice efectul de încasare de pe cartea de credit. Din acest motiv se numește "efect secundar".

Practicanții "codului curat" vor sări să arate spre acest exemplu ca dovadă a nevoii de a scrie funcții mici cu nume bine alese. Însă doar această măsură, în opinia mea, pare prea slabă pentru un aspect atât de important. De ce ? Pentru că nu ne putem baza pe o soluție ce necesită atenția și subiectivitatea umană.

Ideea din spatele stilului funcțional este de a adăuga mai multă structură programelor pe care le scriem.

Cum am putea adăuga mai multă structură codului, astfel încât codul de mai sus să exprime și încasarea de pe cartea de credit?

Soluția este să nu executăm acțiunea de încasare imediat, ci să o "reificăm" ( concretizăm). Vom crea o valoare care reprezintă operațiunea, iar acea valoare o vom folosi în viitor pentru execuția efectivă a acțiunii. În acest mod, vom putea adăuga această informație la semnătură, mai exact: putem returna efectul afară din funcție pe calea tipului de retur.

def buyCoffee(cc: CreditCard): (Coffee, Charge) = {    
  val cup = new Coffee()
  (cup, Charge(cc, cup.price))    
}

Funcția respectă acum proprietățile unei funcții pure. (spre deosebire de funcții impure - funcții care conțin efecte secundare)

Fundamentul programării funcționale este programarea folosind doar funcții pure. Sau mai precis: folosind doar funcții care respectă transparența referențială, o proprietate definită prin păstrarea comportamentului unui program în urma înlocuirii apelului unei funcții cu valoarea returnată de către funcție, prin evaluarea apelului. În primul exemplu de cod, după o astfel de înlocuire, am pierde efectul de încasare de pe credit card, obținând astfel o cafea gratis.

O consecință a programării în acest stil e faptul că vom putea folosi doar date imutabile. Modificarea câmpurilor sub forma suma += x e considerat efect secundar conform definiției de TR.

Înainte să mergem mai departe să studiem câteva exemple de efecte din situații foarte comune în programarea imperativă, și cum le-am putea reifica în stil funcțional.

E posibil ca cititorul perspicace să fi observat deja faptul că toate funcțiile cu efecte secundare care au fost transformate în funcții pure prin reificare, au tipul de retur de forma :

F[B]

Este un mod mai formal de a exprima, faptul că funcția produce o valoare de tip B, dar pentru a efectua calculul trebuie trecut prin efectul F. Un mod intuitiv de a imagina acest mecanism, e să ne gândim la F ca la un container, sau la o cutie. Când deschidem cutia, pentru a folosi valoarea din interior, o acțiune se va executa. F descrie această acțiune.

Aceasta este structura adițională, pe care programarea funcțională o adaugă. Toate limbajele funcționale (fie că au tipuri stricte, fie că au tipuri dinamice) au o strategie de a extrage efectul în mod explicit, astfel încât să fie înțeles de către compilator / interpretor.

De ce e grea programarea funcțională ?

În programarea imperativă forma unei funcții este mai îngăduitoare

Să presupunem că avem funcțiile f și g de forma :

f :: A => B

g :: B => C

Putem să le folosim, (compunem / legăm una de alta), apelând prima funcție f, să luăm rezultatul obținut și să îl folosim ca argument de intrare pentru funcția g, în acest mod : g(f(a)

Dacă funcțiile au efecte secundare, acest model de compoziție este permisiv, însă nu foarte bun, deoarece nu avem exprimat sub o formă structurală efectul secundar. Pur și simplu îl ignorăm fără a ţine cont de el în compunerea funcțiilor. Prin această omisiune gravă, riscăm să lăsăm softul să dezvolte comportament emergent necontrolat.

În PF, datorită reificării, prin care obținem control asupra efectelor, funcțiile vor avea forma :

f :: A => F[B]

g :: B => F[C]

Sau chiar mai complex:

f :: A => F[B]

g :: B => G[C]

În acest scenariu, compoziția obișnuită a funcțiilor nu mai funcționează, fiindcă nu putem lua rezultatul lui f, și să îl folosim direct în g.Vom avea nevoie de logică adițională pentru despachetarea, împachetarea și transformarea valorii relevante, astfel ca f și g să poată fi legate.

Aceasta face o Monadă. Poate ați auzit de acest concept până acum. Sunt doar niște proprietăți speciale care trebuie implementate de efectul F, pentru a putea lega (compune) mai multe funcții cu rezultate în domeniul F.

Dacă în exemplele de funcții de mai sus F ar fi o Opțiune, compunerea f cu g ar însemna că în cazul în care f nu întoarce nici o valoare, nu are sens să apelăm g mai departe, iar rezultatul apelului compunerii nu ar întoarce la rândul lui nici o valoare.

Structura funcțiilor și a datelor în programarea funcțională se numește "formă" (eng. shape). Similar cu formele din domeniul geometric, aceste forme au relații între ele cu o fundamentare matematică puternică.

Să ne reamintim că geometria matematică are forme simple: triunghi, pătrat, dreptunghi, cerc, cât și forme abstracte, complexe : spirale, elipse, poliforme, poligoane, elicoide, sferoide, etc.

Programarea funcțională este despre forme și transformări între ele. E despre recunoașterea comportamentului logic din software și împărțirea lui în forme care ulterior vor fi compuse împreună în mod conștient pe baza proprietăților lor matematice.

Dificultatea principală în acest domeniu vine din învățarea tuturor formelor existente, numelor și aplicabilității lor practice. O conversație tehnică a unor programatori funcționali, sună ca o limbă străină, datorită termenilor folosiți:

monoid, functor, transformare naturală, monadă, limită, adjuncție, comonadă, algebră, coalgebră, catamorfism, anamorfism, lentilă, profunctor, final, cofinal

Proveniența lor este dintr-o ramură a matematicii numită Teoria Categoriilor care este o formă abstractă de matematică ce încearcă să unească mai multe ramuri de matematică, care sunt deja la rândul lor abstracte. Conceptele dezvoltate în acest domeniu s-au dovedit să aibă o aplicabilitate largă în domeniul dezvoltării software.

În acest articol, nu putem merge mai departe cu explicații privitor la aceste construcții abstracte, dar sper că v-am stârnit apetitul și curiozitatea de a afla mai multe.

Cuvinte de încheiere

Programarea funcțională poate fi considerată geometria programării, fiind o metodă științifică de a construi software.

E o practică care țintește să ne facă mai productivi, ajutându-ne să trăim o viaţă mai ușoară, cu stres redus și mai puține buguri. E o metodă de a controla structura softului evitând o moarte prin o mie de tăieturi date de comportamentul emergent al complexității accidentale.

Dați-i o șansă, e posibil să descoperiți că iubiți din nou programarea.