Sissejuhatus funktsionaalsesse programmeerimisse
Sissejuhatus
Seni on põhirõhk olnud objektorienteeritud programmeerimisel ehk maailma modelleerimisele objektidena, mis hoiavad endas olekut ning pakuvad käitumist läbi meetodite. Java nägi ilmavalgust 1995 aastal ning oli juba eos disainitud kui objektorienteeritud keelena, kus klassid ja objektid olid kõikide teiste featuuride aluseks.
Samas kõik programmeerimiskeeled aegamööda arenevad. Java puhul see tähendas seda, et on üle võetud ideid teistest programmeerimise paradigmadest. Alates Java 8st, mis tuli välja 2014 aastal, on üks nendest paradigmadest olnud funktsionaalne programmeerimine.
Seega toetab Java lisaks objektorienteeritud programmeerimisele ka teistsugust paradigmat: funktsionaalset programmeerimist.
Programmeerimise paradigmasid saab laias laastus jagada kahte liiki:
Imperatiivsed keeled kirjeldavad kuidas saavutada mingit tulemust ehk samm-sammuliselt koos selgesõnalise juhtvooga.
Deklaratiivsed keeled kirjeldavad, milline tulemus peab olema ning selle saavutamine jäetakse käitusaja vastutada.
Kuigi Java on peamiselt objektorienteeritud keel, siis sellel on tugev funktsionaalse programmeerimise tugi olemas ning need kaks lähenemist ei välista teineteist. Tänapäevane Java kood ühendab sageli mõlemat paradigmat: kood ja rakenduse domeen struktureeritakse objektidena ning funktsionaalset stiili kasutatakse andmete töötlemiseks.
Antud peatüki eesmärk on tutvustada, mis asi on funktsionaalne programmeerimine, kuidas Java käsitleb seda ning milliseid tööriistu on võimalik selle raames kasutada.
Imperatiivne vs deklaratiivne stiil
Toome näiteks ühe lihtsa ülesande: eesmärk on leida kõik töötajad inseneeriaosakonnast.
Traditsiooniline, imperatiivne lähenemine kirjeldab samm-sammult, kuidas seda teha:
List<Employee> result = new ArrayList<>();
for (Employee employee : employees) {
if (employee.getDepartment().equals("Engineering")) {
result.add(employee);
}
}
Deklaratiivne lähenemine kirjeldab hoopis, mida soovitakse saavutada:
List<Employee> result = employees.stream()
.filter(e -> e.getDepartment().equals("Engineering"))
.collect(Collectors.toList());
Mõlemad annavad sama tulemuse. Deklaratiivne versioon keskendub eesmärgile - "filtreeri töötajatest inseneeriosakonna töötajad välja ning kogu nad loendisse" - ilma detailset implementatsiooni kirjeldamata (ehk kuidas töötajaid itereerida jne.). Nõuete kasvamisel (näiteks nende seast veel midagi välja filtreerida või hoopis sorteerida) jääb deklaratiivne kood reeglina loetavamaks võrreldes imperatiivse vastega.
Puhtad funktsioonid
Funktsionaalne programmeerimine põhineb kindlal arusaamal sellest, milline üks funktsioon olema peaks.
Objektorienteeritud programmeerimises loevad ja muudavad meetodid sageli objekti olekut, see ongi kapseldamise mõte. Funktsionaalne programmeerimine lähtub vastupidisest põhimõttest: funktsioon peaks sõltuma ainult oma sisenditest ja andma ainsa tulemusena väljundi, ilma kõrvalmõjudeta. Sellist funktsiooni loetakse puhtaks funktsiooniks.
Puhas funktsioon annab sama sisendi korral alati sama väljundi ega muuda iseendast väljaspool midagi. Andmed liiguvad sisse, tulemus liigub välja - muud mõju ei ole.
Näiteks:
// Pure — depends only on its argument, changes nothing
public static int square(int x) {
return x * x;
}
Võrdluseks ebapuhas funktsioon:
private int callCount = 0;
// Impure — modifies external state as a side effect
public int square(int x) {
callCount++;
return x * x;
}
Teist versiooni on raskem testida ja seda õigustada: funktsiooni kahekordne väljakutsumine ei ole samaväärne ühekordsega ning tulemus sõltub varjatud olekust, mida väljakutsuja ei näe. Puhtatel funktsioonidel selliseid varjatud sõltuvusi ei ole.
Muutumatus (immutability)
Kuigi puhtad funktsioonid ei muuda endast väljaspool olevat olekut, siis kuidas on lood andmetega, mille peal nad töötavad? Kui neid andmeid saab programmi teistes osades vabalt muuta, ei saa funktsioon enam tagada, et sama sisend annab alati sama väljundi. Seetõttu seob funktsionaalne programmeerimine puhtad funktsioonid muutumatuse põhimõttega. See tähendab, et olemasoleva objekti muutmise asemel tagastavad operatsioonid uue objekti, milles on uuendatud väärtused. Näiteks:
// Mutable — the original list is modified in place
employees.sort(Comparator.comparing(Employee::getSalary));
// Immutable — the original list is untouched, a new sorted stream is produced
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary))
.collect(Collectors.toList());
Muutumatuid andmeid on turvalisem edastada, lihtsam testida ning neid on hea kasutada mitmelõimelises koodis, sest puudub jagatud muudetav olek, mida erinevad lõimed võiksid rikkuda.
Kõrvalmõjud (side effects)
Puhtad funktsioonid ei muuda midagi väljaspool iseennast ning muutumatuid andmeid ei ole võimalik kohapeal muuta. Tegelikes programmides peab nende andmetega ikkagi lõpuks midagi ette võtma, näiteks need andmebaasi kirjutama või nende põhjal teavitusi välja saatma. Selliseid välismaailmaga toimivaid ja vaadeldavaid tegevusi nimetatakse kõrvalmõjudeks.
Kõrvalmõju on tegevus, mida funktsioon teeb lisaks väärtuse arvutamisele ja tagastamisele, olgu selleks siis välja muutmine, faili kirjutamine, konsoolile printimine, võrgupäringu tegemine jne. Kõrvalmõjud on reaalsetes programmides vältimatud, kuid funktsionaalne programmeerimine soovitab nendele kindlad piirid ette seada.
Suurem osa andmetöötlusloogikast - filtreerimine, teisendamine, agregeerimine - on sageli võimalik kirjutada puhaste funktsioonidena. Kõrvalmõjud (näiteks tulemuse salvestamine andmebaasi või e-kirja saatmine) tehakse seejärel üks kord lõpus, kasutades puhta töötlusahela tulemust.
Funktsionaalne programmeerimine Javas
Java ei ole puhtalt funktsionaalne keel, kuid alates Java 8 versioonist toetab see funktsionaalset stiili esmaklassiliselt järgmiste vahendite kaudu:
- Funktsionaalsed liidesed - liidesed, millel on üks abstraktne meetod ja mida saab käsitleda väärtustena.
- Lambda-avaldised - lühike süntaks funktsionaalsete liideste realiseerimiseks otse kasutuskohas.
- Stream API - teek deklaratiivsete andmetöötlusahelate loomiseks.
Neid teemasid käsitletakse järgmistes peatükkides.