ABONAMENTE VIDEO REDACȚIA
RO
EN
NOU
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 1
Abonament PDF

Problema efectelor secundare

Ovidiu Deac
Consultant software



PROGRAMARE

Cunoscută şi sub acronimul OOP, programarea obiectuală a fost introdusă în anii ‘60, cu apariţia limbajului Simula67. E paradigma folosită de majoritatea firmelor de software. Limbaje cum ar fi Java, C#, C++, Python, Ruby sunt în topul preferinţele iar stilul de lucru “orientat pe obiecte” este cel mai popular. 

OOP este o paradigma imperativă adică programul descrie felul în care se modifica starea sistemului pe parcursul rulării aplicaţiei. Sistemul este modelat prin clase de obiecte. Fiecare clasa descrie ce variabile de stare au obiectele de tipul respectiv, ce proprietăţi au şi ce acţiuni putem face asupra lor. Prin încapsulare ascundem detaliile de implementare şi astfel utilizatorul clasei este interesat exclusiv de interfaţa expusă de clasa respectivă. Putem modela astfel toate lucrurile din jurul nostru. Pare o abordare foarte naturală, dar totuşi sunt câteva probleme majore cu aceasta. 

Problemele pornesc de la faptul că, în afara metodelor constante, funcţiile membru produc efecte secundare pentru ca ele modifica starea obiectului pe care sunt apelate sau stările altor obiecte la care au acces. Majoritatea codului OOP este scris astfel. 

O funcţie ar trebui să returneze o valoare dar uneori aceasta produce efecte secundare care pot fi: modificarea unor structuri de date din afara funcţiei, interacţiune cu sistemul de operare/maşina virtuală (creare de fişiere, procese sau fire de execuţie), alocare sau dealocare de resurse, aruncarea excepţiilor etc. 

O funcţie care nu produce efecte secundare şi nu e influenţată de exterior se numeşte “funcţie pură” si rezultatul ei depinde exclusiv de parametrii de intrare. 

În cele ce urmează voi discuta despre problemele puse de funcţiile “impure” şi voi încerca sa arăt avantajele construirii aplicatiilor in principal pe funcţiilor pure. 

Testarea 

Variabilele globale mutabile folosite în cod sunt, în general, o sursa de probleme. Indiferent ca ele sunt simple variabile globale sau obiecte singleton, în esenţă reprezintă acelaşi lucru: date mutabile accesibile din mai multe locuri. Acest cod este foarte greu de testat şi de înţeles pentru ca valoarea unei variabile se poate schimba pe neaşteptate. Astfel pot apărea nenumărate căi de execuţie care nu sunt evidente şi pe care pentru a le putea înţelege trebuie sa parcurgem cod din afara funcţiei respective. 

Mai mult, putem spune că, în general, o funcţie care are efecte secundare este greu de testat deoarece comportamentul sistemului depinde de ordinea în care sunt produse efectele secundare deci pentru a testa funcţia trebuie sa o testăm în toate scenariile posibile în care ar putea fi apelată alături de celelalte funcţii care produc efecte secundare de tipul respectiv. 

Îmbunătăţirea testabilităţii sistemului se poate face folosind predominant funcţii pure. Acestea sunt de multe ori trivial de testat pentru că nu necesita un setup/teardown pentru că nu depind de componente externe. Rezultatul produs depinde exclusiv de parametrii de intrare. 

Paralelismul 

În momentul în care un obiect este accesat concurent din mai multe fire de execuţie avem nevoie de un mecanism de sincronizare. 

Ideal ar fi ca firele de execuţie să fie total independente, altfel sincronizarea va face ca sistemul sa nu fie folosit la capacitate maximă. Orice interacţiune între ele încetineşte sistemul. Alta problema este coerenţa cache-urilor în cazul în care procesoare diferite împart date mutabile. Mai mult, dacă avem un sistem distribuit problema e şi mai dificilă deoarece costurile de sincronizare sunt foarte mari. 

Un alt aspect este dificultatea de a scrie şi de a înţelege un cod care rulează pe mai multe fire de execuţie. Bugurile de sincronizare sunt poate cele mai dificile fiind sunt de multe ori surprinzătoare şi greu de reprodus. 

Ideal ar fi ca obiectele folosite din mai multe fire de execuţie sa fie imutabile iar datele transmise thread-ului prin mesaje asincrone. Astfel programatorul nu se loveşte de complexitatea lucrului cu primitivele de sincronizare ci lucrează la un nivel mai înalt şi mai uşor de înţeles. 

Paternul acesta este cunoscut sub numele de “thread pool pattern”. Fiecare worker-thread primeşte un mesaj care descrie jobul pe care îl are de făcut, şi returnează un rezultat pe care îl pune în alta coada. Este esenţial ca thread-ul sa nu modifice date din exterior ci sa producă un rezultat pe care să îl pună într-o coada de rezultate. 

Folosind o coada de mesaje standard a limbajului folosit, problemele noastre sunt rezolvate. Putem scrie relativ uşor o aplicaţie paralelă sau una distribuită, datorită în primul rând faptului ca worker-ul nostru nu produce efecte secundare. El produce doar un rezultat. 

Să mergem mai departe şi să presupunem ca worker-ul respectiv are acces în exterior doar la date imutabile. Asta înseamnă ca la pornire îşi poate face o copie a datelor respective nu va avea nevoie de sincronizare. Referitor la paralelismul masiv faptul că nu trebuie sincronizate firele de executie constituie un avantaj important. 

Execuţia asincronă 

În multe situaţii avem nevoie să executăm o anumită secvenţă asincron. Apelul asincron va primi pe lângă parametrii normali şi o funcţie care va procesa rezultatele. Acest stil de lucru este destul de dificil în paradigma imperativă. Problema vine de la faptul ca în stilul imperativ e foarte importantă ordinea în care se execută operaţiile. 

Ordinea este importantă tocmai din cauza faptului ca funcţiile cu care se lucrează au efecte secundare iar ordinea în care sunt produse efectele secundare e importantă. 

În lipsa efectelor secundare problema se transforma în a stabili felul în care datele depind unele de altele. Nu ne interesează cât durează calculul şi nici ordinea în care se fac calculele respective atâta timp cât rezultatele sunt disponibile când e nevoie de ele. Astfel în cazul unui apel asincron obţinem un rezultat de tip “promisiune” (future) care va fi folosit probabil pentru calculul unui alt future etc. 

Optimizarea 

Teoria spune ca optimizarea ar trebui făcută după ce funcţionalitatea dorită e implementată dar unele optimizări sunt foarte dificil de făcut dacă codul scris se bazează pe efecte secundare. 

În urma măsurătorilor tragem concluzia că se justifică sa apelăm o funcţie f în paralel. Dacă implementarea funcţia f este pură schimbarea din execuţie secvenţială în execuţie paralelă este mult mai simple comparativ cu situatia cand f produce efecte secundare. De asemenea, funcţiile pure ne dau posibilitatea de schimba relativ uşor anumite calcule din sincron în asincron faze târzii ale dezvoltării aplicaţiei. 

Pe lângă paralelizare şi execuţie asincronă putem face uşor şi alte optimizări cum ar fi execuţie lazy sau memoizare. 

Execuţia “lazy”

În unele situaţii dorim să nu executăm un anumit calcul decât dacă este într-adevăr nevoie de acesta, de exemplu încărcarea unui modul să fie facută doar la nevoie (adică “evaluare lazy”). 

Dacă încărcarea modulului produce efecte secundare aceasta optimizare poate fi dificil sau chiar imposibil de făcut. 

În concluzie pentru a face un anumit calcul lazy în fazele târzii ale scrierii aplicaţiei, mai exact în faza de optimizare, este esenţial ca acesta sa nu producă efecte secundare deci calculul respectiv să fie făcut de o funcţie pură. 

Memoizarea

O alta optimizare frecvent întâlnită este “memoizarea”. Aceasta se aplică în cazuri în care o anumită funcţie e apelată de multe ori cu aceeaşi parametrii. Memoizarea se face prin stocarea rezultatelor calculate şi la apelul următor făcut cu aceeaşi parametrii în loc să refacem calculul returnăm rezultatul calculat anterior. Aceasta optimizare poate fi convenabilă în situaţiile în care apelurile se fac repetat cu un anumit set de parametrii iar căutarea rezultatului între rezultatele calculate anterior este considerabil mai rapidă decât calculul respectiv. 

Dacă funcţia care face calculul cu pricina are efecte secundare memoizarea nu se poate face. În schimb în cazul unei funcţii pure aceasta este aproape trivială. 

Concluzie 

Problema programării orientate obiect vine de la filozofia ei că obiectele îşi modifica starea în timp. Aplicaţiile sunt construite în mare parte pe funcţii care produc efecte secundare, întregul concept de efect secundar fiind tratat superficial în OOP şi în programarea imperativă în general. 

Dacă s-ar da atenţie mai mare efectelor secundare produse de funcţii codul rezultat ar fi mai simplu, mai robust, mai uşor de paralelizat şi de optimizat. 

În articolele următoare din serie vom discuta despre programarea funcţională care diferă de programarea imperativă tocmai prin felul în care abordează problema efectelor secundare.

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