ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
Numărul 150
Numărul 149 Numărul 148 Numărul 147 Numărul 146 Numărul 145 Numărul 144 Numărul 143 Numărul 142 Numărul 141 Numărul 140 Numărul 139 Numărul 138 Numărul 137 Numărul 136 Numărul 135 Numărul 134 Numărul 133 Numărul 132 Numărul 131 Numărul 130 Numărul 129 Numărul 128 Numărul 127 Numărul 126 Numărul 125 Numărul 124 Numărul 123 Numărul 122 Numărul 121 Numărul 120 Numărul 119 Numărul 118 Numărul 117 Numărul 116 Numărul 115 Numărul 114 Numărul 113 Numărul 112 Numărul 111 Numărul 110 Numărul 109 Numărul 108 Numărul 107 Numărul 106 Numărul 105 Numărul 104 Numărul 103 Numărul 102 Numărul 101 Numărul 100 Numărul 99 Numărul 98 Numărul 97 Numărul 96 Numărul 95 Numărul 94 Numărul 93 Numărul 92 Numărul 91 Numărul 90 Numărul 89 Numărul 88 Numărul 87 Numărul 86 Numărul 85 Numărul 84 Numărul 83 Numărul 82 Numărul 81 Numărul 80 Numărul 79 Numărul 78 Numărul 77 Numărul 76 Numărul 75 Numărul 74 Numărul 73 Numărul 72 Numărul 71 Numărul 70 Numărul 69 Numărul 68 Numărul 67 Numărul 66 Numărul 65 Numărul 64 Numărul 63 Numărul 62 Numărul 61 Numărul 60 Numărul 59 Numărul 58 Numărul 57 Numărul 56 Numărul 55 Numărul 54 Numărul 53 Numărul 52 Numărul 51 Numărul 50 Numărul 49 Numărul 48 Numărul 47 Numărul 46 Numărul 45 Numărul 44 Numărul 43 Numărul 42 Numărul 41 Numărul 40 Numărul 39 Numărul 38 Numărul 37 Numărul 36 Numărul 35 Numărul 34 Numărul 33 Numărul 32 Numărul 31 Numărul 30 Numărul 29 Numărul 28 Numărul 27 Numărul 26 Numărul 25 Numărul 24 Numărul 23 Numărul 22 Numărul 21 Numărul 20 Numărul 19 Numărul 18 Numărul 17 Numărul 16 Numărul 15 Numărul 14 Numărul 13 Numărul 12 Numărul 11 Numărul 10 Numărul 9 Numărul 8 Numărul 7 Numărul 6 Numărul 5 Numărul 4 Numărul 3 Numărul 2 Numărul 1
×
▼ LISTĂ EDIȚII ▼
Numărul 82
Abonament PDF

Geometria codului - despre ce e programarea funcțională

Florin Bunău
Senior Software Engineer @ ComplyAdvantage



PROGRAMARE


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.

NUMĂRUL 149 - Development with AI

Sponsori

  • Accenture
  • BT Code Crafters
  • Accesa
  • Bosch
  • Betfair
  • MHP
  • BoatyardX
  • .msg systems
  • P3 group
  • Ing Hubs
  • Cognizant Softvision
  • Colors in projects