# Nemennosť (Immutability)

Nasledujúca kapitola nie je o návrhovom vzore, ale o programovacom princípe, ktorý je natoľko dôležitý, že sme sa mu rozhodli plnohodnotne venovať, nakoľko ho budeme vo veľkom používať vo zvyšku knihy.

Prvá vec po "Hello world", ktorá nám bola každému predstavená, keď sme sa učili programovať, sú premenné. Zistili sme, že je to jedinečný nástroj na tvorbu dynamických programov. Tieto malé škatuľky, ktoré nám boli poskytnuté z pamäti len pre nás, nám dovoľujú hodnoty nastavovať, čítať a dokonca aj modifikovať. Nielenže nám umožňujú zachytiť neustále meniacu sa realitu, ale fungujú rovnako, ako funguje hardvér, v ktorom žijú - počítač nám teda pri ich manipulácii rozumie a funguje rýchlo. Je však naozaj všetko také dobré a ideálne, ako si myslíme?

Ukážme si jednoduchý matematický príklad. Majme 2 premenné a a b, a premennú c, ktorá vyjadruje súčet ich hodnôt. Dosaďme si za a a b čísla 5 a 3 a zapíšme to:

a = 5
b = 3
c = a + b

Je nám okamžite jasné, že premenná c nadobúda v tomto prípade hodnotu 8. Poďme si ukázať ekvivalentný príklad v Jave:

int a = 5;
int b = 3;
int c = a + b;

Ako by sa dalo čakať, premenná c bude tiež nadobúdať hodnotu 8 (avšak neberte nás za slovo - pokojne tento príklad vyskúšajte v JShell!). Zatiaľ je všetko v poriadku, no čo by sa stalo, ak by sme v Jave následne prepísali hodnotu premennej c na niečo iné, napr. 999? Absolútne nič - hodnota by bola vymenená za novú, presne tak, ako sme sa to učili:




 

int a = 5;
int b = 3;
int c = a + b;
c = 999; // This is valid in programming language

Ako by sa niečo takéto dalo vyjadriť v matematickom zápise? Mohli by sme skúsiť napr.:




 

a = 5
b = 8
c = a + b
c = 999 // ??? 8 = 999 ???

No veľmi rýchlo si uvedomíme, že niečo nie je v poriadku - vyšlo by nám, že 8 je rovné 999! Ako je to však možné? V oboch prípadoch sme hovorili o premenných, tak prečo je v programovacom jazyku možné premennú meniť, no v matematike nie? Musíme si uvedomiť zásadný rozdiel - kým v Jave je možné c meniť, lebo v skutočnosti len meníme hodnotu v pamäti, ktorú sme si tak pomenovali, v matematike je premenná c len iné vyjadrenie výrazu a + b. V momente, kedy hodnoty a aj b (nezávislých premenných) určíme, premenná c (závislá premenná) bude okamžite jasne a nemeniteľne daná. Naopak, v programovacích jazykoch sa len hodnoty v dvoch pamäťových blokoch, ktoré sme si pomenovali a a b, sčítajú a uložia do nového pamäťového bloku, ktorý sme pomenovali c.

Berúc toto do úvahy, zistíme, že sme na začiatku klamali - príklady neboli vôbec ekvivalentné, pretože symbol = vyjadruje v matematike rovnosť ľavej a pravej strany výrazu, kým v Jave vyjadruje priradenie hodnoty výrazu na pravej strane do premennej na ľavej strane.

Môže sa zdať, že tento rozdiel vieme ignorovať s tým, že "veď si to pamätáme", no, bohužiaľ, problémov je viac. Ukážme si ďalší príklad - množinovú operáciu zjednotenie. Majmä nasledujúci matematický príklad:

A = { 1, 2, 3 }
B = { 2, 3, 4 }

U = A ∪ B
U = { 1, 2, 3, 4 }

V Jave máme pre reprezentáciu množín dátovú štruktúru Set - konkrétne jeho implementáciu HashSet. Keďže žiadny operátor zjednotenia neexistuje, musíme použiť metódu addAll:

Set<Integer> A = new HashSet<>(Arrays.asList(1, 2, 3));
Set<Integer> B = new HashSet<>(Arrays.asList(2, 3, 4));

A.addAll(B);

Na prvý pohľad sa zdá, že všetko je na rozdiel od predchádzajúceho príkladu v poriadku - premenné A a B sme neupravili. Napriek tomu, ak vypíšeme hodnoty premennej A pred a po zjednotení s B, zistíme, že jej hodnota bola predsalen zmenená:







 
 

> System.out.println(A)
[1, 2, 3]

> A.addAll(B)
true

> System.out.println(A)
[1, 2, 3, 4]

Ako je toto možné? Odpoveď je, že metóda addAll je tzv. deštruktívna - jej zavolaním sa zničí (zmení) vnútorný stav objektu. Opäť je to niečo, čo v matematike nemá obdobu - množina A je jasné daná, ako by sa potom zjednotením s inou mala jej hodnota akokoľvek meniť? To však nie je všetko - všimnite si, že metóda addAll má návratovú hodnotu boolean - true ak bola množina modifikovaná, false v opačnom prípade. Zmena samotnej množiny tak bola tzv. vedľajší účinok (side effect). To sa líši od funkcií v matematike - ak by sme chceli operáciu zjednotenia definovať ako funkciu f, zapísali by sme ju nasledovne:

A, B ⊆ Z
f(A, B) = A ∪ B

Táto funkcia vyprodukuje pre rovnaký vstup vždy rovnaký výsledok a (samozrejme) nevyprodukuje žiadny vedľajší efekt. V programovaní voláme takéto matematické funkcie čisté. Ako sme si všimli, addAll nespĺňa ani jednu z podmienok - je preto nečistá.

To však, bohužiaľ, nie je koniec. Možno najväčší rozdiel v práci (a rozmýšľaní) matematikov a programátorov je spôsob, akým zapisujú riešenia problémov. Ukázať si to môžeme na jednoduchom príklade - vyjadrenie n-tého Fibonacciho čísla. V matematike by sme ho definovali nasledovne:

n ∈ N

n-té Fibonacciho číslo je číslo k také, pre ktoré platí:

n ≤ 2 => k = 1
n > 2 => k = k(n - 1) + k(n - 2)

V Jave by sme, naopak, napísali:

int fibonacciNumber(int index) {
    if (index <= 2) {
        return 1;
    }

    long previousValue = 1;
    long finalValue = 2;

    for (int i = 3; i < index; i++) {
        long newFinalValue = previousValue + finalValue;
        previousValue = finalValue;
        finalValue = newFinalValue;
    }

    return finalValue;
}

Všimnime si zásadný rozdiel - kým v matematike sme definovali, čo znamená pojem "n-té Fibonacciho číslo", v (imperatívnom) programovacom jazyku sme zapísali, ako ho vypočítať. Samozrejme, algoritmus vyššie môžeme prepísať na rekurzívny, no v Jave by sme veľmi rýchlo zistili, že by bol extrémne pomalý a fungoval by len pre malé indexy. Tento rozdiel sa dá popísať ešte inak - matematický zápis (rekurzívny algoritmus) je bezstavový (stateless), kým Javouský cyklus je stavový (stateful).

Na zhrnutie, v procedurálnych (C, Pascal) a objektovo orientovaných jazykoch (Java, C#) sme zvyknutý písať kód, ktorý je:

  • menný - používa premenné, ktorých hodnotu mení aj po ich prvotnom nastavení
  • nečistý - obsahuje množstvo funkcií (metód) s vedľajším efektom
  • stavový - používa stavové konštrukcie (napr. cykly) pre výpočet

Naproti tomu, matematici sú zvyknutý na zápisy, ktoré sú:

  • nemenné - ak je raz premennej určená hodnota, tás hodnota jej aj ostane
  • čisté - všetky funkcie sú bez vedľajších efektov a pre rovnaký vstup vrátia rovnakú hodnotu
  • bezstavové - nikde v matematike neudržiavame aktuálny stav, ktorý meníme; častokrát používame rekurziu na vyjadrenie napr. postupností

Jazyky, ktoré stavajú na týchto myšlienkach, voláme funkcionálne jazyky. Príkladmi sú Haskell (čisté funkcionálne programovanie) a Scala (ktorá je multiparadigmická - spája funkcionálne a objektovo orientované programovanie). V imperatívnych programovacích jazykoch, napr. v Jave, kde sa postupne pridali prvky z funkcionálnych jazykov, hovoríme o programovaní vo funkcionálnom štýle.

Prirodzene sa nám naskytá otázka, prečo sa ideme venovať princípom z funkcionálnych jazykov, keď Java je objektovo orientovaná? Dôvody sú hneď 2 - estetický a praktický.

Z estetického hľadiska, teda z pohľadu čitateľnosti kódu, sme si ukázali, že klasický matematický prístup k popisu riešenia problémov je konzistentnejší, bez skrytých "prekvapení" a prehľadnejší. Nemusíme sa trápiť tým, ako musíme výpočet vykonať, trápime sa tým, ako ho definovať, ako ho opísať. Takto píšeme kód, ktorý je zrozumiteľnejší a lepšie udržiavateľný. A to ocenia nielen veľké tímy, ale aj individuálni programátori, ktorí sa ku svojmu kódu vracajú po určitom čase.

Praktické výhody spočívajú najmä v bezpečnosti a testovateľnosti kódu. Keď máme nemenné triedy, nemusí nás trápiť, čo sa bude s inštanciou diať - nemusíme zbytočne vytvárať defenzívne kópie po celom kóde. Používaním a písaním čistých metód robíme svoj kód podstate viac a jednoduchšie testovateľný. Zároveň (vďaka nemennosti) dostávame skrytú výhodu - kód je zároveň bezpečný pre viacvláknové programovanie. A to je obrovská výhoda v čase, kedy je trendom zvyšovanie počtu jadier oproti výkonu jedného.

Týchto konceptov sa však netreba báť, práve naopak, už ste sa s nimi stretli. Trieda reťazca String je nemenná - všimnite si, že žiadna operácia (vrátane +) nezmodifikuje originálny reťazec, ale vytvorí nový. Podobne je na tom nová knižnica pre dátumy a čas Date/Time API, ktorá bola do Javy pridaná vo verzii 8.

V rámci tejto kapitoly si ukážeme, ako uplatniť nemmenosť (immutability) v našom kóde a ako implementovať čisté metódy. Vzhľadom na limitáciu Javy (resp. absenciu niektorých vlastností) si nemôžeme ukázať napr. využívanie rekurzie v našich algoritmoch. Ak by ste mali záujem, ako sa v takomto štýle programuje, odporúčame knihu Programming in Scala od Martina Oderskeho a kolektívu.

Poznámka

Kompilátory funkcionálnych programovacích jazykov sú postavené tak, aby funkcionálne zápisy optimalizovali pre počítače. Napr. rekurzívne funkcie, ktoré sa končia volaním samých seba (tzv. tail recursion), sú prepísané na obyčajný cyklus. Myšlienka je však v tom, že funkcionálny zápis je deskriptívny a teda je ľahšie porozumiteľný ako jeho imperatívny zápis v cykle, ktorý je vhodný pre samotný počítač. Pamätajme - píšeme programy pre ľudí, nie pre stroje!

# Modelová situácia

Keďže sme spomínali, že nemennosť ide ruka v ruke s matematikou, ukážeme si ju v praxi na implementácii racionálneho čísla. Tento príklad je inšpirovaný z už vyššie spomínanej knihy Programming in Scala od Martina Oderskeho a kolektívu.

Racionálne číslo môžeme definovať ako pomer A / B, kde A je z množiny celých čisel a B je z množiny prirodzených čísel. A označujeme ako čitateľ (angl. numerator) a B ako menovateľ (angl. denominator). Na základe toho môžeme vytvoriť jednoduchú implementáciu:

public class Rational {

    private int numerator;
    private int denominator;

    public Rational(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public int getDenominator() {
        return denominator;
    }
}

Tak, ako sme v úvode spomínali, že číslo päť vždy bude mať hodnotu 5, aj po vypočítaní príkladu 5 + 3, jedna tretina ostatne taktiež vždy 1/3, aj po vypočítaní napr. (1/3) * 2. Preto nedáva zmysel, aby sme nechávali atribúty numerator a denominator ako premenné, ktorých hodnota sa môže v čase meniť. Aby sme docielili, že ich bude možné nastaviť len počas vytvárania objektu, označíme obe (zatiaľ) premenné kľúčovým slovom final:



 
 




public class Rational {

    private final int numerator;
    private final int denominator;

    // Rest ommited for brevity
}

Takýto zápis má za následok, že kompilátor bude hlásiť chybu:

  • ak by sme sa tieto hodnoty snažili po priradení zmeniť
  • ak by sme tieto hodnoty nepriradili vôbec

To nám efektívne zaručí, že atribútom numerator a denominator musia byť hodnoty priradené práve raz, a to v konštruktore počas vytvárania inštancie triedy.

Poznámka:

Iné jazyky môžu nemennosť atribútov riešiť po svojom. C# používa kľúčové slovo readonly ako ekvivalent Javouského final. Scala a Kotlin majú zápis val attributeName, kde val označuje value (ako kontrast pre var, čo označuje variable).

V JShell si teraz môžeme vytvoriť niekoľko ukážok:

> Rational oneThird = new Rational(1, 3);
Defined field Rational oneThird = sk.tuke.fei.kpi.oop.chapters.structural.immutability.Rational@589838eb

> Rational eightFifths = new Rational(8, 5);
Defined field Rational eightFifths = sk.tuke.fei.kpi.oop.chapters.structural.immutability.Rational@6500df86

Vidíme však, že výpis sk.tuke.fei.kpi.oop.chapters.structural.immutability.Rational@589838eb nám nič nehovorí. Aby sme to v rýchlosti "opravili", v triede Rational prepíšeme metódu toString, ktorá vracia čitateľnú reprezentáciu objektu. V praxi si ju viac ukážeme v ďalšej kapitole, nateraz sa uspokojíme len s výsledkom. Po pridaní nasledujúcej metódy do triedy:

@Override
public String toString() {
    return numerator + " / " + denominator;
}

nám už výpis v konzole bude dávať zmysel:

> Rational oneThird = new Rational(1, 3);
Defined field Rational oneThird = 1 / 3

> Rational eightFifths = new Rational(8, 5);
Defined field Rational eightFifths = 8 / 5

Vráťme sa na chvíľu k rozdielom medzi matematikou a Javou. Ak máme 2 rôzne tretiny, vieme, že sú obe totožné, preto môžeme napísať nasledovnú rovnosť.

1/3 = 1/3

V Jave, keď chceme porovnávať hodnoty dvoch premenných, používame metódu equals. Ak ju však použijeme na 2 rôzne tretiny:




 

Rational oneThird = new Rational(1, 3);
Rational anotherOneThird = new Rational(1, 3);

oneThird.equals(anotherOneThird);

Zistíme, že výraz nám vyhodnotí ako false. Ako je to možné? Pretože Java nevie, kedy sú 2 objekty triedy Rational zhodné. Preto ju to musíme naučiť prepísaním metódy equals a definovaním rovnosti:

@Override
public boolean equals(Object obj) {
    // Check if given object is an instance of this class
    if (!(obj instanceof Rational)) {
        return false;
    }

    // Compare numerator and denominator of both rationals
    Rational other = (Rational) obj;
    return denominator == other.denominator
        && numerator == other.numerator;
}

Teraz nám už porovnanie tvoj rôznych inštancií jednej tretiny vráti pravdivú hodnotu true. Viac sa o porovnávaní objektov dozviete v nasledujúcej kapitole.

Než sa presunieme na podrobnejšiu implementáciu triedy, uvažujme ešte, ako by sme konvertovali celé čísla z primitívneho typu int do našej triedy pre racionálne čísla. Celé čísla nie sú nič iné, ako čitatele s "neviditeľným" menovateľom 1. Preto napr. číslo 5 vieme zapísať ako racionálne číslo v tvare new Rational(5, 1). Tŕňom v oku ostáva jednotka - veď predsa nikdy nepíšeme celé čísla v tvare zlomku! Aby sme zlepšili klientský kód, implementujeme ďalší konštruktor, ktorý však bude mať len 1 parameter:

public Rational(int number) {
    this.numerator = number;
    this.denominator = 1;
}

Teraz by sa vám mala rozsvietiť varovná kontrolka DRY princípu - oba konštruktory vyzerajú prakticky rovnako - oplatí sa nám duplikovať kód len kvôli krajšiemu zápisu? Našťastie, Java na nás myslela a dáva nám spôsob, ako interne volať kód iného konštruktora - kľúčové slovo this bude volať v konštruktore ako metódu s príslušnými parametrami - v tomto prípade this(number, 1). Po úprave budú naše konštruktory vyzerať nasledovne:







 


public Rational(int numerator, int denominator) {
    this.numerator = numerator;
    this.denominator = denominator;
}

public Rational(int number) {
    this(number, 1);
}

Odteraz je zápis new Rational(5) validný a slúži ako alias zápisu new Rational(5, 1). Ďalšia podobná vec, ktorú môžeme interne ošetriť, je zjednodušenie zlomku. Okrem estetickej vlastnosti to má výhodu v tom, že na rozdiel od matematiky môžeme v dátovom type int držať len hodnoty do určitej veľkosti - preto zjednodušením zlomku nebudeme týmto cenným priestorom plýtvať.

Ako teda na to? Najprv musíme vypočítať najväčší spoločný deliteľ, ktorým následne čitateľ a menovateľ predelíme. Vytvorme si teda najprv privátnu metódu, ktorá nám tohto deliteľa nájde. Využijeme pri tom Euclidov algoritmus:

private int calculateGCD(int a, int b) {
    int dividend = Math.max(a, b);
    int divisor = Math.min(a, b);
    
    while (divisor > 0) {
        int remainder = dividend % divisor;
        dividend = divisor;
        divisor = remainder;
    }

    return dividend;
}

Túto hodnotu následne vypočítame do dočasnej premennej, ktorou už len predelíme čitateľa a menovateľa. Všimnime si, že pri výpočte najväčšieho spoločného deliteľa sme použili absolútnu hodnotu čitateľa - to preto, lebo euklidov algoritmus funguje len pre kladné čísla.


 




public Rational(int numerator, int denominator) {
    int gcd = calculateGCD(Math.abs(numerator), denominator);
    this.numerator = numerator / gcd;
    this.denominator = denominator / gcd;
}

O správnosti fungovania kódu sa môžeme presvedčiť nasledujúcim príkladom:

> Rational a = new Rational(12, 8);
Defined field Rational a = 3 / 2

> Rational b = new Rational(9, 21);
Defined field Rational b = 3 / 7

Ostáva nám posledná vec ohľadom atribútov racionálneho čísla - ich definičné obory. Z definície, ktorú sme napísali vyššie, nám vyplýva, že čitateľ numerator nemusíme nijako kontrolovať, no aktuálne je možné nastaviť menovateľa denominator na záporné čísla, či dokonca nulu, čo v žiadnom prípade nie je platné racionálne číslo. Aby sme takéto situácie ošetrili, musíme v hlavnom konštruktore skontrolovať hodnotu menovateľa a v prípade neplatnej hodnoty vyhodiť chybu, aby klientský kód vedel, že robí niečo zlé. Java nám na také situácie ponúka výnimku IllegalArgumentException, ktorú použijeme nasledovne:


 
 
 






public Rational(int numerator, int denominator) {
    if (denominator < 1) {
        throw new IllegalArgumentException("Denominator must be a natural number!");
    }

    int gcd = calculateGCD(Math.abs(numerator), denominator);
    this.numerator = numerator / gcd;
    this.denominator = denominator / gcd;
}

Výborne - každá inštancia triedy Rational nám teraz reprezentuje nemenné a validné racionálne číslo. Aktuálne je nám však trošku zbytočná - veď nevieme nad ňou robiť žiadne operácie! Poďme sa pozrieť na to, ako implementovať operáciu sčítania.

Ak by sme mohli modifikovať vnútorné premenné, možno by nás lákalo urobiť niečo nasledovné:

public void add(Rational other) {
    this.numerator = numerator * other.denominator + other.numerator * denominator;
    this.denominator = denominator * other.denominator;
}

My však už vieme, že zlomok svoju hodnotu meniť nemôže, preto výsledok (návratová hodnota) operácie sčítania musí byť nová inštancia triedy Rational. Preto metódu add prepíšeme nasledovne:

public Rational add(Rational other) {
    return new Rational(
            numerator * other.denominator + other.numerator * denominator,
            denominator * other.denominator
    );
}

Metóda add je teraz čistá - pre rovnaký vstup vypíše vráti rovnaký výstup a zároveň nemá žiadne vedľajšie efekty!

Čo však, ak by sme chceli k zlomku pripočítať celé číslo? Vďaka nášmu alternativnému konštruktoru by sme súčet 1/3 + 5 vedeli zapísať ako oneThird.add(new Rational(5)). Vyzerá to ale trošku neprehľadne - namiesto jedného znaku pre číslo päť sme ich použili 15. Preto preťažíme pôvodnú implementáciu metódy add novou, ktorá akceptuje ako parameter dátový typ int:

public Rational add(int number) {
    return new Rational(
            numerator + number * denominator,
            denominator
    );
}

Interne sa nám takto dvakrát opakuje prakticky ten istý kód. V zmysle DRY princípu tak môžeme výpočet delegovať pôvodnej metóde. Inými slovami - vytvoríme alias:

public Rational add(int number) {
    return add(new Rational(number));
}

Majte však na mysli, že takto vytvárame novú inštanciu objektu, ktorá bude okamžite po dokončení metódy stratená. V istých prípadoch to môže byť zbytočné - pokojne by sme mohli nechať pôvodnú implementáciu, ktorá sčítava zlomok s celým číslom priamo.

# Best practices

  • Automaticky označujte lokálne a globálne premenné modifikátorom final, aby sa stali nemennými
  • Modifikátorom final môžete pokojne označiť aj parametre metódy - takto zaručíte, že ich hodnoty ani náhodou neprepíšete
  • Ak máte pocit, že potrebujete pracovať s premennou, ktorej hodnotu treba v čase meniť (teda je nutne udržiavať informáciu o nejakom stav), skúste najprv porozmýšľať, či sa problém nedá vyriešiť bezstavovo a nemenne
  • Ak odpoveď na otázku vyššie je nie, resp. potrebujete menné premenné kvôli výkonu aplikácie, odstráňte modifikátor final z príslušných premenných

# Úlohy

# 1. Doplňte ďalšie operácie na triede Rational

V modelovej situácii sme ukázali, ako môžeme implementovať sčítanie dvoch racionálnych čísel. Teraz je rad na vás, aby ste si to vyskúšali sami a rozšírili triedu Rational o nasledujúce metódy:

public double toDouble();

public Rational negate();

public Rational sub(Rational other);

public Rational sub(int other);

public Rational mul(Rational other);

public Rational mul(int other);

public Rational div(Rational other);

public Rational div(int other);

Vysvetlivky:

  • toDouble nám konvertuje zlomok na desatiné číslo
  • negate nám vráti opačnú hodnotu
  • sub vykoná operáciu odčítania s Rational alebo int
  • mul vykoná operáciu násobenia s Rational alebo int
  • div vykoná operáciu delenia s Rational alebo int

Snažte sa neduplikovať kód, ale šikovne využívať už existujúce metódy. Inšpirujte sa príkladom - operácia odčítania (a - b) je to isté, ako pripočítanie opačnej hodnoty (a + (-b)).

# 2. Vytvorte objektovú reprezentáciu matematickej funkcie

Keď sa už toľko oháňame s matematikou, poďme sa zamerať na jednu zo základných nástrojov tejto vedy - funkcie. Konkrétne si ukážeme funkcie v oboru reálnych čísel - čo v Jave znamená dátový typ double. Autori jazyka na nás mysleli a do štandardnej knižnice implementovali rozhranie Function. Tej však chýbajú mnohé operácie ako napr. sčítanie 2 funkcií, preto sme sa rozhodli, že ju obalíme vlastnou implementáciou, ktorá tieto vlastnosti pridá.

Vytvorte triedu Func, ktorá bude pozostávať z vnútornej Function funkcie. To by vám malo umožniť vytvárať triedu vo funkcionálnom štýle, napr. kvadratickú funkciu by ste vedeli zapísať ako new Func(x -> x * x). Zabezpečte, aby vaša implementácia mala nasledujúce metódy:

  • apply, ktorá zavolá vnútornú Function.apply a vráti výsledok
  • add, ktorá vráti funkciu rovnú súčtu vnútornej a poskytnutej funkcie
  • sub, ktorá vráti funkciu rovnú rozdielu vnútornej a poskytnutej funkcie
  • mul, ktorá vráti funkciu rovnú násobku vnútornej a poskytnutej funkcie
  • compose, ktorá vráti funkciu rovnú zloženiu vnútornej s vonkajšiou (rovnaká implementácia ako Function.compose)
  • negate, ktorá vráti funkciu vracajúcu opačnú hodnotu vypočítanej vnútornou funkciou

Zároveň preťažte metódy, aby prijímali Func, Function aj double ako svoj argument.

Svoju implementáciu si môžete vyskúšať napr. nasledovným spôsobom:

Func f = new Func(x -> x * x);
Func g = new Func(Function.identity()).mul(2);

// h(x) = x^2 + 2*x - 5
Func h = f.add(g).sub(5);

// 30
h.apply(5);

// -2
h.apply(1);

// 3
h.apply(-4);

# 3. Definujte štandardné funkcie ako konštanty pre pohodlie programátorov

Ak by sme začali písať kód, veľmi rýchlo by sme zistili, že niektoré štandardné funkcie sa nám budú opakovať, napr. kvadratická či lineárna. Okrem porušenia DRY princípu tu smrdí aj čitateľnosť kódu - význam new Func(x -> x * x) je síce jasný, ale zápis Func.QUADRATIC je predsalen krajší.

Vytvorte statické premenné v triede Func pre štandardné a často použivané funkcie (aspoň pre elementárne). Nezabudnite ich označiť public static final a ich mená napísať veľkými písmenami. Zároveň, kde je to možné, využite existujúce implementácie niektorých funkcií v Javouskej triede Math.

# 4. Konvertujte objekt matematickej funkcie na funkcionálne rozhranie

Všimnime si zápis vytvárania novej inštancie triedy Func:

Func f = new Func(x -> x + 2);

Takýto zápis je možný vďaka tomu, že konštruktor akceptuje Javouské funkcionálne rozhranie Function. Keby sme chceli vytvoriť separátnu inštanciu tejto vnútornej reprezentácie funkcie, vyzeralo by to takto:

Function<Double, Double> f = x -> x + 2;

Bližšie sa už k matematickej reprezentácii nedostaneme - je to určite lepšie, ako naše obalenie tohto rozhrania. Našťastie, niečo podobné vieme využiť aj my - ako sme už spomenuli v paragrafe vyššie, musíme vytvoriť naše vlastné funkcionálne rozhranie. A to je podstatou tejto úlohy.

Vytvorte funkcionálne rozhranie Fun, ktoré bude mať všetky vlastnosti ako trieda Func. V nej implementujte 1 abstraktnú metódu apply, ktorá prijme a vráti výsledok v dátovom type double. Všetky ďalšie metódy pre kombináciu funkcii implementujte s modifikátorom default - to vám umožní zachovať rozhranie funkcionálne. Na záver pridajte štandardné funkcie nie ako konštanty, ktoré sa v rozhraní vytvoriť nedajú, ale ako statické metódy (a zmeňte názvy tak, aby ste dodržali konvencie pre pomenúvavanie metód). Ak neviete, kde máte začať, môžete použiť túto ukážku:

@FunctionalInterface
public interface Fun {

    // abstract `apply` method is missing - add it!
    
    default Fun add(Fun other) {
        return x -> apply(x) + other.apply(x);
    }

    static Fun cubic() {
        return x -> x * x * x;
    }
}

Poznámka

Toto rozhranie môžete označiť anotáciou FunctionalInterface, ktoré vám vyhodí kompilačnú chybu, ak budete mať viac ako 1 abstraktnú metódu.

Svoju implementáciu si môžete vyskúšať napr. nasledovným spôsobom:

Fun f = x -> x*x + 5*x;
Fun g = Fun.exponential();

// h(x) = e^( 5*x^2 + 25*x )
Fun h = g.compose(f.mul(5));

// 1
h.apply(0);

// ~ 708.04
h.apply(0.25);

// ~ 936589.2
h.apply(0.5);

# Riešenia úloh

Nasledujúce kódy obsahujú riešenia zo všetkých úloh.

# Pozri tiež

# Diskusia