I-ASOS Prednáška 9

Funkcionálne programovanie – Java 8 Lambda Expressions

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 )

Priklad 1.

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:

V čom je rozdiel?

  • For cyklus presne určuje, že účty sú spracovávané postupne za sebou a v akom poradí.
  • Z funkcionalneho zapisu to nie je jasné. To je však výhoda, pretože tak nechávame na JVM aby sama určila poradie, a ak má viac CPU spacovala ich paralelne.

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);});

Aké výpočty by da dali paralelizovať?

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

    • výstupom je nová kolekcia objektov (rovnakej veľkosti ako vstupná kolekcia)

    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

  • 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.

  • 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:

Priklad 2.

Vytvorenie stream reťazcov s riadkami textového súboru a ich výpis pomocou forEach

collect vytvorí zo streamu kolekciu

map na vytvorenie streamu s dĺžkami riadkov a ich následný výpis pomocou forEach

reduce na sčítanie dĺžok riadkov (a výpis veľkosti súbora.)

alebo zistenie dĺžky najdlhšieho riadku

filter na odstránenie prázdnych riadkov a výpis neprázdnych pomocou forEach

flatMap na vytvorenie streamu slov a nasledné zistenie počtu rôznych slov pomocou distinct a count

Zhrnutie

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.

    • výstup: nová kolekcia, obsahujúca tie členy vstupnej kolekcie, pre ktoré je predikát pravdivý.
  • flatMap

    • aplikuje funkciu f: x ∊ T => f(x) ∊ Sn t.j. výstup f(x) je kolekcia objektov typu S.

    • výstupná kolekcia je kolekcia objektov typu S, ktorá vznikne spojením kolekcii f(x) pre všetky objekty vstupnej kolekcie.
  • forEach

    • aplikuje funkciu void f ( T x ).

    • Výstupom metódy forEach nie je nová kolekcia ani žiadna hodnota, ako výstup môže využiť len side-efekty.

      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.