Funkcionálna programovacia paradigma je typická pre funkcionálne jazyky ako Lisp. Určitú podporu pre funkcionálne programovanie však ponúkajú aj mnohé nefunkcionálne jazyky (Python, Java, R,…) a tiež Java 8.
Pre funkcionálne programovanie je charakteristické, že s funkciami sa pracuje rovnako ako s inými dátovými objektami (či hodnotami). Napriek istým syntaktickým rozdielom, umožňujú mnohé jazyky zápis funkcií spôsobom veľmi blízkym matematickému zápisu – lambda výrazy.
Využitie funkcionálneho programovania v Java8 a jeho súvislosť s paralelnými výpočtami si ilustrujeme na krátkom príklade: (niečo podobné je aj v https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public static void main(String[] args) { List<Ucet> ls = new ArrayList<>(); ls.add(new Ucet("U001", 100)); ls.add(new Ucet("U002", 200)); ls.add(new Ucet("U003", 300)); ls.add(new Ucet("U004", 400)); // pripocitanie uroku ku kazdému uctu for (Ucet u : ls) { u.pripocitajUrok(); } // vypis stavov na ucte for (Ucet u : ls) { System.out.println(u); } } |
Pozn. Triedu Ucet si môžete stiahnuť tu Ucet.java
Kolekcia stream (ktorú priniela Java 8) umožňuje používať funkcionálne programovanie a nahradiť for-cykly v programe nasledovne:
1 2 |
ls.stream().forEach(u->u.pripocitajUrok()); ls.stream().forEach(u->System.out.println(u)); |
V čom je rozdiel?
Pozn.
horeuvedené príkazy možno spojiť aj do jediného: ls.stream().forEach(u-> {u.pripocitajUrok(); System.out.println(u);});
paralelné spracovanie podporuje kolekcia parallelStream ls.parallelStream().forEach(u-> {u.pripocitajUrok(); System.out.println(u);});
Stream je špeciálna kolekcia objektov, ktorá poskytuje pre funkcionálne programovanie okrem forEach aj dalšie metódy (ktoré by bolo možné vykonávať paralelne),
Map – aplikuje samostatne na každý objekt v kolekcii funkciu s návratovou hodnotou nejakého typu S
Príklad: Ak by sme chceli získať len prehľad o výške vkladov na jednotlivých účtoch, mohli by sme to urobiť jediným príkazom
1 |
Stream<Integer> rs = ls.stream().map(u -> u.getStav()); |
Reduce ak by sme chceli zistiť ďalej celkový súčet vkladov na účtoch, mohli by sme to urobiť jediným príkazom reduce ktorým na kolekciu čísel aplikujeme opreráciu súčtu.
1 |
double sucet = rs.reduce(0.0, (a,b)->a+b); |
Tento výpočet je možné vďaka asociativite operácie súčet tiež paralelizovať.
Pozn. horeuvedené dva kroky príklade môžeme spojiť do jedného map-reduce príkazu:
1 |
double sucet = ls.stream().map(u->u.getStav()).reduce(0.0, (a,b)->a+b); |
Vytvorenie stream reťazcov s riadkami textového súboru a ich výpis pomocou forEach
1 2 3 4 5 6 7 8 9 10 11 |
public static void main(String[] args) { String fileName = "src/javaapplication270/JavaApplication270.java"; try (Stream<String> stream = Files.lines(Paths.get(fileName))) { stream.forEach(l->System.out.println(l)); } catch (IOException e) { e.printStackTrace(); } } |
collect vytvorí zo streamu kolekciu
1 |
List<Integer> ll = stream.collect(Collectors.toList()); |
map na vytvorenie streamu s dĺžkami riadkov a ich následný výpis pomocou forEach
1 2 |
Stream<Integer> is = stream.map(l-> l.length()); is.forEach(l->{System.out.println(""+l);}); |
reduce na sčítanie dĺžok riadkov (a výpis veľkosti súbora.)
1 2 |
Stream<Integer> is = stream.map(l-> l.length()); System.out.println("Velkost suboru:" + is.reduce(0,(a,b)->a+b);); |
alebo zistenie dĺžky najdlhšieho riadku
1 |
int len = stream.map(l-> l.length()).reduce(0,(a,b)->Math.max(a,b)); |
filter na odstránenie prázdnych riadkov a výpis neprázdnych pomocou forEach
1 |
stream.filter(l-> !l.isEmpty()).forEach(l->{System.out.println(l);}); |
flatMap na vytvorenie streamu slov a nasledné zistenie počtu rôznych slov pomocou distinct a count
1 2 |
Stream<String> slova = stream.flatMap(s-> Arrays.asList(s.split(" ")).stream()); System.out.println("Pocet roznych slov: "+slova.distinct().count()); |
Aké výpočty možno paralelizovať?
Algoritmy, v ktorých sa aplikuje ten istý výpočet (funkcia) na sadu (kolekciu, stream) objektov s rovnakou štruktútou (typom) možno veľmi jednpducho paralelizovať. Preto jazyky a frameworky poskytujú na podporu paralelizácie špeciálne – paralelné/distribuované kolekcie. Tieto kolekcie majú metódy umožňujúce paralelné vykonávanie rôznych typov výpočtov (z matematického pohľadu funkcií) nad nimi.
Java8 poskytuje možnosť využitia funkcionlneho programovania pre kolekciu Stream<T>. Jej verzia ParallelStream umožňje aj paralelizáciu na počítačoch s viacerými jadrami. Stream-metódy majú ako argument funkciu, príp. lambda výraz, ktorý aplikujú a členy kolekcie.
map
aplikuje funkciu f: x ∊ T => f(x) ∊ S samostatne na každý objekt v kolekcii.
výstup: nová kolekcia rovnakej veľkosti (členy výstupnej kolekcie sú typu S)
Pozn. Funkcie tu zapisujeme a chápeme formálne ako v matematike: t.j ako zobrazenie (predpis) priraďujúce prvku jednej množiny (objektu jedného typu) prvok druhej množiny.
reduce
aplikuje binárnu operáciu f: (x,y) ∊ TxT => f(x,y) ∊ T celú kolekciu objektov typu T a zredukuje ju na jediný objekt typu T.
výstup: objekt typu T.
binárna operácia f musí byť asociatívna aj komutatívna.
filter
aplikuje predikát p: x ∊ T => p(x) ∊ {true,false} samostatne na každý objekt v kolekcii.
flatMap
aplikuje funkciu f: x ∊ T => f(x) ∊ Sn t.j. výstup f(x) je kolekcia objektov typu S.
forEach
aplikuje funkciu void f ( T x ).
Pozn. Keďže void funkcia nemá návratovú hodnotu, nie je to funkcia v matematickom zmysle.
Ďalšie užitočné java8-stream metódy, ktoré nemajú funkcionálny argument:
count
limit
collect
Všetky tieto metódy možno rozdeliť do 2 kategórií podľa toho čo je ich výstupom
Transformácie – výstup je nový stream
Akcie – výstup je hodnota.