Jedeme v Kotlinu!

 

Proč byste si měli přečíst následující články? Protože u nás se Kotlin osvědčil.

Nerozhodujeme se, jestli Java nebo Kotlin, nové věci píšeme v Kotlinu.

Proto se chceme podělit o pár zkušeností.

Kotlin a generické typy

List<String> strings = Arrays.asList("Preco", "nie?");
List<Object> objects = (List<Object>) (List) strings;

Uvedený kód je validná Java.

Generické typy sú zložitejší ale užitočný koncept. Umožňujú vytvárať abstrakcie nad určitými problémami tak, aby zároveň bola zachovaná silná typová kontrola. Nanešťastie ich implementácia v Jave prispieva k tomu, že mnohí programátori sa môžu cítiť pri použití generických typov zmätene a uchýliť sa k niečomu takému, ako kód na začiatku tohto článku.

 

Základná terminológia

interface Collection<E>  { 
  public void add (E x);  
  public Iterator<E> iterator(); 
}

Collection je generický typ s jedným typovým parametrom E. Collection<String> je inštancia generického typu. String je v tomto kontexte typový argument generického typu Collection.

 

Wildcard typy

Jedna z príčin komplikovanosti generických typov v Jave sú wildcard typy.

public static <T> void sort(List<T> list, Comparator<? super T> c)

Všetky hodnoty všetkých inštancií generického typu Comparator, ktorých typový argument je predok typu T sú typu Comparator<? super T>. Napr. všetky hodnoty typov Comparator<Integer>, Comparator<Number> a Comparator<Object> sú typu Comparator<? super Integer>, pretože Integer, Number aj Object sú predkami typu Integer. To dáva zmysel. Na usporiadanie zoznamu celých čísel (Integer) je možné použiť aj Comparator, ktorý vie porovnávať celé čísla (Integer), aj Comparator, ktorý vie porovnávať ľubovoľné čísla (Number), aj Comparator, ktorý vie porovnávať ľubovoľné objekty (Object).

Typy, ktorých aspoň jeden argument je wildcard, sa nazývajú wildcard parameterized types.

Kotlin pristupuje ku generickým typom iným spôsobom. Wildcard typy sú nahradené typovými projekciami (type projections) a varianciou v mieste deklarácie (declaration-site variance).

 

Problém

Niekedy je vhodné implementovať kód, ktorý je rovnaký pre viaceré inštancie generického typu a typový argument môže byť ľubovoľný, alebo môže spĺňať určité jednoduché podmienky – obvykle implementovať určité rozhranie.

public interface Entity {
  Long getId();
}

public class User implements Entity

public class Document implements Entity

Môže byť potrebné previesť zoznam objektov typu User na množinu id. Podobne môže byť potrebné previesť zoznam objektov typu Document na množinu id. Obe úlohy by mohla riešiť jedna metóda.

public class Entities {
  public static Set<Long> toIdSet(List<Entity> entities) {...}
}

Avšak očakávaný spôsob použitia nebude funkčný.

List<User> users = Arrays.asList(user1, user2);
Set<Long> userIds = Entities.toIdSet(users);

Na druhom riadku nahlási kompilátor chybu: incompatible types: java.util.List<User> cannot be converted to java.util.List<Entity>.

Generické typy boli do Javy pridané až vo verzii 1.5. Aby bolo možné interagovať zo starým kódom, ktorý generické typy nepoužíval, je možné pretypovať hodnotu inštancie generického typu na hodnotu typu bez typového argumentu (napr. List<User> na List). Zároveň je možné pretypovať hodnotu typu bez typového argumentu na hodnotu typu z ľubovoľným typovým argumentom (napr. List na List<Entity>). Kombináciou týchto dvoch postupov je možné pretypovať hodnotu inštancie generického typu s nejakým typovým argumentom na hodnotu inštancie rovnakého generického typu ale s ľubovoľným iným typovým argumentom. To je to, čo robí hack uvedený na začiatku článku. Môžeme ho použiť aj v prípade users a Entities.toIdSet a zdanlivo bude všetko fungovať.

List<User> users = Arrays.asList(user1, user2);
Set<Long> userIds = Entities.toIdSet((List<Entity>) (List) users);

Čo by sa mohlo stať keby kompilátor v takomto prípade nehlásil chybu incompatible types? Keby hodnota typu List<User> bola použiteľná všade tam, kde sa očakáva List<Entity>?

List<User> users = new ArrayList<>(Arrays.asList(user1, user2));
List<Entity> entities = (List<Entity>) (List) users;
entities.add(new Document());
User user3 = users.get(2);

Kompilácia prebehne v poriadku. Problém je v tom, že entities je ten istý zoznam ako users. Do zoznamu entities je možné pridať aj inú entitu ako entitu typu User a tú je potom možné vybrať zo zoznamu users. Vybraná entita však nebude očakávaného typu User a program vyhodí ClassCastException.

Riešenie v Jave

Java na riešenie takýchto problémov zaviedla wildcard typy.

public class Entities {
  public static Set<Long> toIdSet(
      List<? extends Entity> entities
  ) {...}
}

Metódu je možné použiť očakávaným spôsobom.

List<User> users = Arrays.asList(user1, user2);
Set<Long> userIds = Entities.toIdSet(users);

V čom sa List<? extends Entity> líši od (List<Entity>) (List)?

List<User> users = new ArrayList<>(Arrays.asList(user1, user2));
List<? extends Entity> entities = users;
entities.add(new Document());
User user3 = users.get(2);

Kompilátor nahlási chybu na treťom riadku:

no suitable method found for add(Document)
method java.util.Collection.add(capture#1 of ? extends Entity) is not applicable
(argument mismatch; Document cannot be converted to capture#1 of ? extends Entity)

Dá sa to chápať tak, že metóda add na objekte typu List<? extends Entity> vyžaduje argument istého typu, ktorý je potomkom typu Entity. Kompilátor tento typ nazýva capture#1 of ? extends Entity. O tomto type je známe iba to, že jeho predkom je typ Entity. O potomkoch typu nie je možné povedať nič, a preto nie je možné vytvoriť hodnotu daného typu. Metóda add na objekte typu List<? extends Entity> nie je použiteľná.

 

Riešenie v Kotline pomocou typovej projekcie

V Kotline nastáva rovnaký problém.

fun toIdSet(entities: MutableList<Entity>): MutableSet<Long>
val users = mutableListOf(user1, user2)
val userIds = toIdSet(users)

Kompilátor nahlási chybu: Type mismatch: inferred type is MutableList<User> but MutableList<Entity> was expected.

Riešenie pomocou typovej projekcie je ekvivalentné k riešeniu v Jave.

fun toIdSet(entities: MutableList<out Entity>): MutableSet<Long>

MutableList<out Entity> v Kotline zodpovedá List<? extends Entity> v Jave.

Drobný rozdiel je možné vidieť v chybovej hláške, ktorú kompilátor vypíše v mieste chybného použitia.

val entities = users as MutableList<out Entity>
entities.add(Document())

Na druhom riadku nahlási: Out-projected type ‚MutableList<out Entity>‘ prohibits the use of ‚public abstract fun add(element: E): Boolean defined in kotlin.collections.MutableList‘.

 

Riešenie v Kotline pomocou variancie v mieste deklarácie

Nie je možné voľne pretypovať MutableList<User> na MutableList<Entity>. Problém vznikne pri použití metódy ako add na MutableList<Entity>. Pretypovanie na MutableList<out Entity> zakazuje použitie metód ako add. Môžeme si však predstaviť generický typ, ktorý by metódy ako add vôbec nemal. Napr. List v Kotline je práve taký typ. Potom je bezpečné v každom kontexte predpokladať, že List<User> je potomok List<Entity>.

fun toIdSet(entities: List<Entity>): List<Long>
val users = listOf(user1, user2)
val userIds = toIdSet(users)

Uvedený kód sa kompiluje bezchybne.

val entities = users as List<Entity>
entities.add(Document())

Metóda add na type List neexistuje, preto: Unresolved reference: add.

Metódu toIdSet je samozrejme možné implementovať ako extension method na List<Entity>.

fun List<Entity>.toIdSet(): Set<Long>
val users = listOf(user1, user2)
val userIds = users.toIdSet()

Kovariancia, kontravariancia a ďalšie múdre slová

Pre List v Kotline platí, že pre každý typ T, ktorý je predkom typu S, je List<T> predkom typu List<S>. Toto je bezpečné vďaka tomu, že List neobsahuje žiadne “metódy ako add”. Presnejšie (ale stále zjednodušene) je možné povedať, že je to vďaka tomu, že typový parameter typu List sa nepoužíva ako vstupný parameter žiadnej metódy z typu List. Takejto vlastnosti sa hovorí kovariancia, tj. typový parameter v type List je kovariantný alebo typ List je kovariantný vo svojom typovom parametri.

Kovariancia typového parametra musí byť explicitne uvedená v definícii pomocou modifikátora out.

public interface List<out E> : Collection<E>

Kontravariancia je opakom kovariancie. V Kotline sa explicitne uvádza v definícii pomocou modifikátora in.

interface Filter<in T> {
  fun filter(element: T): Boolean
}

Ak máme hodnotu typu Filter<Entity> môžeme ju bezpečne použiť všade tam, kde potrebujeme Filter<User>. Pre každý typ T, ktorý je predkom typu S, je Filter<S> predkom typu Filter<T>. Toto je bezpečné vďaka tomu, že (zjednodušene) typový parameter typu Filter sa nepoužíva ako výstupný parameter žiadnej metódy z typu Filter. Typový parameter v type Filter je kontravariantný alebo Filter je kontravariantný vo svojom typovom parametri.

Typový parameter typu MutableList sa používa aj ako vstupný aj ako výstupný. Je teda tzv. invariantný, alebo MutableList je invariantný vo svojom typovom parametri. Všetky generické typy v Jave sú invariantné vo svojich typových parametroch.

Variancia je vlastnosť typového parametra. Typy, ktoré majú viac typových parametrov, môžu byť v každom parametri inak variantné. Napr. typ ktorý v Kotline reprezentuje funkciu s jedným parametrom – Function1 – má dva typové parametre. Jeden určuje typ vstupného parametra funkcie a Function1 je v ňom kontravariantný a jeden určuje typ výstupného parametra funkcie a Function1 je v ňom kovariantný.

public interface Function1<in P1, out R> : Function<R> {
  /** Invokes the function with the specified argument. */
  public operator fun invoke(p1: P1): R
}

Napr., hodnota typu Function1<Entity, Long> môže byť použitá všade tam, kde sa očakáva Function1<User, Any>.

 

To muselo dát práce, přitom taková blbost

Na prvý pohľad sa môže zdať, že zmeny v práci s generickými typmi, ktoré prináša Kotlin sú iba kozmetické. Z mojich skúseností však vyplýva, že varianciou v mieste deklarácie sa dá vyriešiť veľká väčšina problémov s generickými typmi, ktoré boli v Jave. Kód, ktorý tak vznikne je intuitívny a neobsahuje nič navyše.

Rozdiel medzi typovými projekciami a wildcard typmi je iba takpovediac filozofický. Vedie však k výrazne menej kryptickým chybovým hláškam a to nie je málo.

Generické typy sú silný nástroj, ktorý umožňuje ešte zlepšiť automatickú kontrolu korektnosti programu. Bola by škoda, keby ostali nevyužité len kvôli tomu, že sú nepochopené.

Zobrazit více

Andrej Herich

Kotlin, Java Developer

Jurij Hlaďuk

Kotlin, Java Developer

Extension funkce v Kotlinu

Taky jste tak nadšení s extension funkcí? Když jsem s Kotlinem začínal, byl jsem unešen, že stačí zmáčknout klávesovou zkratku a hledat funkci, kterou potřebuji. Najednou jsme odstranili většinu utility tříd a vymazali zbytečné závislosti na externích knihovnách, protože to Kotlin už měl v sobě.

Já bych zde však více přiblížil funkce apply,also,let,run a with. Funkce jsou si velmi podobné a přitom každá trochu jiná. No a kdy jakou funkci použít?

Začal bych funkcí let. Tuto funkci nejčastěji uvidíte při práci s nullable objektem nebo pokud transformujeme nullable objekt na jiný nullable objekt. Ušetří nám to klasické podmínky typu x != null. Rozhodně špatné použití by bylo tuto funkci zbytečně několikrát zanořovat a nechat defaultní parametr it. Kód je nakonec nepřehledný i samotné IDE nám radí si tento parametr přejmenovat. To samozřejmě neplatí jen pro let, ale libovolné lambdy s parametry.

 

val information = getPerson()?.let{
    someService.getAdditionalInformation(it)
}

 

Další oblíbená funkce je apply, která se nejčastěji používá, když chceme objekt ještě nějak modifikovat, než vrátíme jeho instanci. Využití má nejčastěji při inicializaci nebo při vytváření builderu. Neočekávané použití by bylo v tomto bloku kódu volat jiné funkce, které nesouvisí s aplikováním změn na objekt.

 

fun person(block: Person.() -> Unit) = Person().apply(block)

 

V případě, že chceme provádět dodatečnou validaci nebo další operace, použijeme funkci also. Zde by se naopak neměly provádět změny jako při funkci apply. Jedině tyto dvě funkce vždy vrací ten samý objekt. Rozdíl jako takový je nakonec jen v this/it.

 

customer.also { 
    validate(it)
    println(it.name)
}

 

Jedna z posledních funkcí je run, která se používá pro operace s objektem nebo taky pro všechny ostatní možnosti, které vás napadnou. Viděl jsem například i takové použití.

attribute?.let{
    println("your attribute: $it")
} ?: run { println("attribute not set")}

 

Pro někoho je možná lepší si napsat vlastní funkci jako je například tato, která nedělá nic jiného, než že provede blok kódu nebo vyhodí výjimku, pokud byla instance null. Hodně to může připomínat Optional<T>. Zkrátka zde se meze nekladou, ale často si člověk vystačí s již existujícími funkcemi.

 

fun <T> T?.orThrow(ex:() -> Exception,block: T.() -> Unit)  = this?.block() ?: throw ex()

 

Poslední je funkce with, do které nemůžeme dávat nullable objekt a je velmi podobná funkci apply. Kterou tedy zvolit? Zde jedině záleží, jakou chceme návratovou hodnotu. Zatímco apply vrací vždy ten samý objekt, with může vracet cokoliv.

Samozřejmě se tyhle funkce nemusí vůbec používat a lze to zapsat jinak, ale v některých případech to zpříjemní čitelnost kódu jako v téhle ukázce.

 

createFile().apply {
    name = "filename.txt"
    path = "/"
}.also {
    println("Writing into file $it.name")
}.run {
    appendText("Hello Artin")
}
Zobrazit více

To, proč je Kotlin tak návykový, je směsicí několika věcí

 

V Artinu rádi prozkoumáváme nové věci a Kotlin nebyl výjimkou. Dostali jsme se k němu před pár lety a od té doby se (nejen u nás) lavinovitě rozšířil. U nových projektů neřešíme otázku, jestli Java nebo Kotlin, odpověď je jasná: Kotlin. U starších projektů zase hledáme cestu, jak alespoň nové featury psát v Kotlinu. Bývá pak těžké odolat pokušení přepsat tu a tam nějaký starší kód z Javy do Kotlinu, zvlášť když stačí v IDEI zmáčknout tu správnou klávesovou zkratku, výsledek trošku poupravit a soubor je najednou poloviční a lépe čitelný.

Proč je Kotlin tak návykový, je směsicí několika věcí. Co však jde z Kotlinu cítit ze všech stran, je pragmatičnost. Řeší věci, které programátory opravdu trápí, a nezabředává do akademických problémů (jako třeba Scala).

Jedna z mých oblíbených funkcionalit v Kotlinu jsou operace nad kolekcemi. Tuto oblast pokrývá několik knihoven. Ty, které jsme aktivně používali, zde popíšu s jejich výhodami a nevýhodami.

Jako jednoduchý (ale v praxi nepoužitelný) příklad pro srovnání operací nad kolekcemi může posloužit konstrukt, který má na vstupu List s čísly a jehož výsledkem je jiný List, ve kterém jsou jen původní čísla menší než 5 a poté vynásobená dvěma.

 

Groovy

Jako jedny z prvních se operace nad kolekcemi objevily v Groovy, které jsme ale používali pouze v menších projektech, testech nebo skriptech. Do větších projektů jsme se s Groovy moc nepouštěli.

V Groovy by náš příklad mohl vypadat třeba takto:

def input = [1, 2, 3, 4, 5, 6, 7, 8, 9] // ArrayList
def output = input
    .findAll { it < 5 }  // nový ArrayList[1, 2, 3, 4]
    .collect { it * 2 }  // nový ArrayList[2, 4, 6, 8]

 

I když to z kódu není na první pohled patrné, Groovy funguje tak, že při každé operaci vytváří nový ArrayList. Ve většině případů to není problém, ale pokud byste se tomu chtěli vyhnout, nenabízí Groovy žádnou alternativu.

Groovy nabízí velmi kompaktní zápis a základní sadu funkcí pro filtrování, transformování a spoustu dalších, ale při delším používání vám některé začnou chybět.

 

Guava FluentIterable

Pak přišla knihovna Guava, která nám vystačila na dlouhou dobu se svým FluentIterable, Function a Predicate. Před příchodem lambd v Java 8 byla Guava hodně ukecaná, s jejich zavedením je zápis kompaktnější.

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> output =
FluentIterable.from(input)     // nové FluentIterable
   .filter(it -> it < 5)       // nové FluentIterable
   .transform(it -> it * 2)    // nové FluentIterable
   .toList();                  // nový ArrayList

 

FluentIterable nabízí sadu funkcí, pro jejich použití je však potřeba vždy FluentIterable vytvořit pomocí některé z metod FluentIterable.from() nebo FluentIterable.of(), což zápis prodlužuje. Pro získání výsledku např. v podobě List je třeba zavolat metodu toList(). V API FluentIterable taky chybí některá funkcionalita a není čistý způsob, jak ji tam přidat.

FluentIterable v Guavě funguje z pohledu vytváření nových objektů přesně naopak než Groovy. Metody filter() a transform() vracejí vždy jen nové FluentIterable, které je view nad tím předchozím. Celý chain se provede a nový List se vytvoří až se zavoláním toList(). Tento „streamovací“ styl jde kombinovat se „zhmotňováním“ výsledků, kde je to potřeba, ale API na to není připraveno a v praxi tento přístup není moc čitelný.

 

Java Streams

S příchodem Streams v Java 8 jsme doufali, že všechny problémy Guavy budou vyřešeny a budeme mít lepší funkcionalitu přímo v JDK. Po prvotním experimentování jsme ale brzy narazili a znovu se vrátili ke Guavě, která s pomocí lambd nabrala nový dech.

List<Integer> input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> output = input.stream()      // nový Stream
    .filter(it -> it < 5)                  // nový Stream
    .map(it -> it * 2)                     // nový Stream
    .collect(Collectors.toList());         // nový List

 

Na první pohled vypadá vše uhlazeně a je to hodně podobné zápisu v Guavě výše. První zádrhel je, že Stream se dá použít (obdobně jako Iterator) pouze jednou. Když se jednou přečte, nelze ho číst znovu, musí se vytvořit nová instance. Jeho použití je pak velmi omezené jako parametr metody, kdy by tato metoda mohla Stream zavolat jen jednou. Nemohla by se např. nejdřív zeptat, jestli je Stream prázdný, a až pokud by nebyl, zavolat na něj nějakou další metodu.

Streamy taky trpí tím, že se autoři snažili vyřešit všechny problémy, včetně těch, které neexistují. Spousta interní logiky Streamu se např. zabývá optimálním vyhodnocováním Streamu ve více threadech – funkcionalita v praxi jen obtížně představitelná. Tato a další obecná logika pak komplikuje použití v jednoduchých, běžně používaných případech a výsledkem je složitý zápis, vysoká časová náročnost a spousta vytvořených objektů. Guava pak v tomto Streams s přehledem překoná a to ani neměla možnost si přizpůsobit JDK knihovnu jako Streams.

 

Kotlin

Když jsem poprvé pracoval s Kotlinem, překvapilo mě, jak je oblast práce s kolekcemi standardní knihovny dobře a uceleně pokrytá. A jsem z toho nadšený pořád. Pamatuji si jen na pár případů, kdy mi nějaká funkce chyběla, a nevím, jestli už to nebyla moje rozmazlenost.

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
val output = input
    .filter { it < 5 }        // nový List [1, 2, 3, 4]
    .map { it * 2 }           // nový List [2, 4, 6, 8]

 

Na našem krátkém příkladu jde vidět velká podobnost zápisu s Groovy a stejně tak je obdobná funkčnost. V Kotlinu se při každé operaci taky vytváří nový List, ale Kotlin zároveň nabízí interface a funkcionalitu kolem Sequence, což je obdoba Iterable se stejnou myšlenkou jako FluentIterable z Guavy.

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
val output = input.asSequence()     // nová Sequence
    .filter { it < 5 }              // nová Sequence
    .map { it * 2 }                 // nová Sequence
    .toList()                       // nový List [2, 4, 6, 8]

 

Při použití Sequence se při každé operaci vrátí nová Sequence, která obaluje tu předchozí. Zhmotnění výsledku se provede až finální operací, což je v našem případě funkce toList(), která vytvoří nový List s výsledkem. Mezi světem List a Sequence pak jde jednoduše přecházet pomocí funkcí asSequence() a toList() a to i v průběhu podle potřeby.

Další z obrovských výhod a ukázka ucelenosti Kotlinu je možnost přidat další operace nad List nebo Sequence s tím, že jejich použití se pak nijak neliší od těch vestavěných. Lze na to použít extension funkce.

fun Iterable<Int>.filterLessThan(number: Int): ArrayList<Int> {
    return filterTo(ArrayList(), { it < number })
}

 

S nově nadefinovanou extension funkcí pak můžeme napsat:

val input = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9) 
val output = input
 .filterLessThan(5)        // nový List [1, 2, 3, 4]
 .map { it * 2 }           // nový List [2, 4, 6, 8] 

 

Závěr

Na jednoduchých příkladech jsme si ukázali možnosti práce s kolekcemi v Kotlinu a porovnali s jinými jazyky a knihovnami. Kotlin nabízí nadprůměrně obsáhlou a velmi ucelenou knihovnu pro práci s kolekcemi s možností jejího dalšího rozšíření podle potřeby.

Pro nás je to jedna z mnoha věcí, která dělá práci v Kotlinu jednodušší a proč Kotlin preferujeme před Javou a ostatními jazyky v JVM.

Zobrazit více

Pavel Černocký

 Kotlin, Java Senior Competency Leader

Jarda Müller

Kotlin, Java Developer

Java potřebovala vylepšit

Tohle platilo skoro vždycky a vlastně platí o všem. A Java na sobě pracovala. Ve verzi Java 5, tedy roku 2005, se konečně objevily generiky. Ne každému se líbilo řešení, které si Sun vybral, ale i tak to bylo “heuréka!”, konečně. Poprvé Java výrazně zasáhla do jazyka jako takového, do jeho syntaxe a hned to byl nový svěží moderní jazyk.

Jenže to bylo 2005. Po tom se dlouho nic nedělo, Java 7 sice přinesla diamond operátor, ale to je jen takový cukřík. Přitom způsob psaní kódu se měnil. Funkcionální programování bylo na vzestupu, procesory dostaly více jader a multithreading, mluvilo se o immutable strukturách, rychlých event driven serverech, a Java nic. V podstatě až do vydání Java 8 se nestalo nic zajímavého, co by reflektovalo změny v IT světě. Dokonce i C# (the dark side) byl modernější praktičtější jazyk.

 

Vlaštovky frustrace

To nezůstalo bez odezvy. Java svět byl vždycky vynalézavý, a tak se začaly objevovat jazyky, které sice nebyly Java, ale mohly běžet na Java Virtual Machine, protože se překládaly do stejného bytecode jako Java.

 

Groovy

Groovy vyšel roku 2007 a poměrně rychle získal určitou popularitu. Byl to dynamický jazyk jako např. Python nebo Ruby a měl pár velmi povedených vlastností. Např. closures, pár extra metod v běžných třídách jako “collect” a “each” v kolekcích. Psát testy v Groovy bylo jako zjevení zářné budoucnosti.

Nicméně Groovy nikdy neměl a nemohl nahradit Javu. Byl prostě určený pro jiné použití – testy, skriptování…

 

Scala

Scala si získala alespoň zájem každého, kdo se zajímal o funkcionální programování. Do té doby snad nikdo nevěřil, že ty akademicky krásné myšlenky z Haskelu se budou dát použít i v normálním životě. A ejhle.

Ale taky to nikdy nebyl jazyk, který byste bez váhání použili místo Javy.

 

Java 8

No a pak konečně (2014, to je vážně po dlouhé době) vyšla Java 8 s closures a streams. Konečně nějaká inovace, konečně funkcionální programování. Java 8 se vážně povedla, tedy na to co můžete vyždímat z jazyka, který byl designován v roce 1996.

Jenže tou dobou už hodně programátorů zahlédlo jinou cestu. Java je fajn jazyk, ale Java Virtual Machine je vážně skvělá věc, a neomezuje se jen na Javu. Groovy, Scala a ostatní ukázaly, že polyglot programming je proveditelné, je super a na JVM vážně sedí. A někdo se rozhodl, že je čas udělat jazyk, který bude mít právě ten účel, pro který se používá Java. A ten někdo byli JetBrains a jazyk Kotlin (no jo, já vím, i Ceylon to zkoušel, ale to se neujalo).

Cílem nebylo nahradit Javu (to se rozhodne každý sám), ale navrhnout jazyk, který bude prostě “lepší Java”. Do Kotlinu přebrali všechno, co se osvědčilo. Z Javy, Groovy, Scaly, a určitě nejen odsud. Kotlin má krásně vyvedené closures a snadnou migraci z Javy (ala Groovy), hodně myslí na immutability a funkcionální programování (ala Scala), typovou inferenci s podporou pro IDE (ostatně je to JetBrains). Získal si podporu Spring frameworku, Gradle… No a následují už jen samá pozitiva a sociální jistoty.

Takže teď, když já říkám, že dělám “java projekt”, tak tím vlastně myslím, že dělám projekt na JVM. A jazyk ve kterém to budu psát? Kotlin nejspíš, testy možná v Groovy, umím si předslavit i Clojure na specifické problémy. Bude tam Java? Ne. Tam kde bych použil Javu, použiju “lepší Javu” – Kotlin.

Zobrazit více

Kotlin a výsledky operácií

V Effective Java sa píše: Use exceptions only for exceptional conditions. (Použite výnimky iba vo výnimočných prípadoch.) Ďalej vysvetľuje: A well-designed API must not force its clients to use exceptions for ordinary control flow. (Dobre navrhnuté API nesmie nútiť svojich klientov používať výnimky na bežné riadenie toku programu.) Čo je vhodné použiť v prípadoch, ktoré nie sú výnimočné?

Napríklad, prihlásenie používateľa do aplikácie by mohlo zlyhať preto, že používateľ s daným menom neexistuje, alebo preto, že zadané heslo sa nezhoduje s uloženým. Implementácia by mohla byť urobená pomocou výnimiek.

public void authenticate(String username, String password)
        throws UserNotFoundException, NotAuthenticatedException

Ak používateľské meno a heslo pochádzajú z vstupného formulára, ktorý vypĺňa človek, nesprávne napísané meno alebo heslo nie sú výnimočný prípad. Je potrebné zobraziť chybovú hlášku.

try {
    authenticate(username, password);
    showMessage("Authenticated as " + username);
} catch (UserNotFoundException ex) {
    showMessage("User not found");
} catch (NotAuthenticatedException ex) {
    showMessage("Not authenticated");
}

 

API núti klienta zachytávať výnimky na zobrazenie chybovej hlášky. Okrem toho, že to nie je v súlade s dobrými mravmi (s Effective Java), tento návrh robí ďalšiu dekompozíciu zložitou, napr. nie je jednoduché dosiahnuť, aby volanie metódy authenticate, získavanie chybovej hlášky a volanie showMessage boli na rôznych miestach. Použitie výnimiek nie je vhodné.

 

Effective Java navrhuje dve riešenia:

 1. state-testing method (metóda na testovanie stavu)

 2. distinguished return value (zvláštna návratová hodnota)

 

Prihlásenie používateľa by malo byť transakčné, kontrola existencie používateľa a kontrola hesla v samostatnej metóde nedáva zmysel. Jednoduché riešenie je použitie enumu ako návratovej hodnoty.

public enum AuthenticationResult {
    OK, USER_NOT_FOUND, NOT_AUTHENTICATED
}
public AuthenticationResult authenticate(
    String username, String password
)

 

Napríklad, implementovať získavanie hlášky pre používateľa v samostatnej metóde je teraz jednoduché.

public String getAuthenticationMessage(
        AuthenticationResult result
) {
    switch (result) {
        case OK: return "Authenticated"
        case USER_NOT_FOUND: return "User not found"
        case NOT_AUTHENTICATED: return "Not authenticated"
        default: throw new IllegalArgumentException(
                "Unknown result: " + result
        )
}

 

Prvým problémom uvedenej implementácie je, že v prípade úspešného prihlásenia nie je dostupné používateľské meno. Je možné ho pridať ako argument metódy getAuthenticationMessage, ale v prípade, že výsledok bude iný ako OK, argument bude úplne zbytočný. Lepšie by bolo, keby úspešný výsledok prihlásenia obsahoval informácie o prihlásenom používateľovi.

Druhým problémom uvedenej implementácie je, že v prípade, že by vznikol nový možný výsledok prihlásenia (napr. nový chybový stav), metóda getAuthenticationMessage by stále bola kompilovateľná a pri behu programu, ak by nový výsledok prihlásenia nastal, došlo by k vyhodeniu výnimky.

Koncept sealed class v Kotline umožňuje oba problémy vyriešiť. Triedy v Kotline (bez modifikátora) sú final z hľadiska Javy, tj. nie je možné vytvárať potomkov k týmto triedam. Triedy s modifikátorom open môžu mať potomkov. Triedy s modifikátorom sealed môžu mať potomkov, ale všetci potomkovia takej triedy musia byť definovaný v rovnakom súbore ako trieda s modifikátorom sealed.

sealed class AuthenticationResult {
    class Ok(val user: User) : AuthenticationResult()
    object UserNotFound : AuthenticationResult()
    object NotAuthenticated : AuthenticationResult()
}

 

UserNotFound a NotAuthenticated nemusia byť definované ako triedy. Je výhodné využiť kľúčové slovo object, ktoré definuje singleton. Nemá zmysel, aby existovala viac než jedna inštancia UserNotFound a viac než jedna inštancia NotAuthenticated.

Trieda AuthenticationResult.Ok obsahuje pole user. Implementácia metódy authenticate do tohto poľa nastaví prihláseného používateľa.

fun authenticate(
        username: String, password: String
): AuthenticationResult {
    val user = userRepository.getUserByName(username)
    return when {
        user == null -> AuthenticationResult.UserNotFound
        user.passwordHash != password.sha256hash() ->
                AuthenticationResult.NotAuthenticated
        else -> AuthenticationResult.Ok(user)
    }
  }

 

Implementácia getAuthenticationMessage v Kotline s využitím sealed class je podobná, ako v Jave s enumom. V Kotline je navyše možné implementovať ju ako extension method (rozširujúcu metódu) na AuthenticationResult.

fun AuthenticationResult.getAuthenticationMessage(): String =
        when (this) {
            is AuthenticationResult.Ok ->
                    "Authenticated as ${user.name}"
            AuthenticationResult.UserNotFound ->
                    "User not found"
            AuthenticationResult.NotAuthenticated -> 
                    "Not authenticated"
        }

 

Keďže UserNotFound a NotAuthenticated sú singletony odporúča sa nepoužiť is. Pri vyhodnocovaní when nastane volanie equals, nie kontrola typu.

Prvý problém je vyriešený vďaka tomu, že trieda AuthenticationResult.Ok má pole user. Za povšimnutie stojí, že vo vetve is AuthenticationResult.Ok -> nastane pretypovanie this na AuthenticationResult.Ok a vďaka tomu je možné použiť this.user.name. this je implicitné, preto stačí user.name.

Druhý problém je vyriešený vďaka tomu, že kompilátor pozná všetkých potomkov AuthenticationResult. Pri kompilácii when výrazu kontroluje, či sú pokryté všetky vetvy. Ak vznikne nový možný výsledok prihlásenia (tj. nový potomok AuthenticationResult), v metóde getAuthenticationMessage vznikne kompilačná chyba.

Na koncept sealed class sa dá pozerať ako na rozšírenie enumu, vďaka ktorému “enumová hodnota” môže mať parameter. To je možné výhodne využiť pre účely návratových hodnôt rôznych operácií. Uvedený príklad bol triviálny a výhodnosť sealed class sa v tomto kontexte nemusí javiť ako zásadná, ale u veľkých aplikácií je bežné, že obsahujú veľké množstvo operácií, ktoré sa vzájomne volajú a každá z nich môže zlyhať mnohými spôsobmi a takéto zlyhania nie sú výnimočné. V takom prípade sealed class výrazne zlepší čitateľnosť kódu a vedie programátora k implementácii korektného správania aplikácie v bežných chybových stavoch.

Koncept sealed class je dobrou ukážkou toho, ako korektne implementované statické typovanie môže zlepšiť kvalitu kódu a pohodlnosť programovania. Samotná sealed class by však nebola veľkým pomocníkom. To ako spolupracuje s ostatnými konceptami z Kotlinu – singleton object, when výraz, chytré pretypovanie, atď. – z neho robí taký silný nástroj. Možno nie je také dôležité, aké jednotlivé vlastnosti má programovací jazyk, ale akým spôsobom sú tieto vlastnosti prepojené a ako spolupracujú. To je to, čo je v návrhu Kotlinu zvládnuté výborne.

Zobrazit více

Andrej Herich

Kotlin, Java Developer

Tomáš Masník

Kotlin, Java, JavaScript Lead

Co nám do projektu přinesl Kotlin

Představte si takový standardní Java projekt. Spring Boot, MVC, Hibernate, SQL databáze, LDAP, nějaké integrace, JS frontend. Čím by se to tak dalo vylepšit…? 🙂 Co třeba Kotlin? Nechci tu vypisovat seznam všeho, kde nám Kotlin pomohl. Dá se docela rychle dohledat, co všechno je na něm cool. Tak jen vypíchnu, co se mně osobně líbilo na tomhle projektu nejvíc. Aby toho nebylo moc, zaměřím se na to, co jsme použili při psaní testů.

 

Přehlednější názvy testů

Poté co jsem nějakou dobu dělal v JavaScriptu, jsem si uvědomil, jak hezky strukturované testy se tam dají psát. Ne, že by to v Javě nešlo, ale tak elegantní jako v Kotlinu to rozhodně není. Kombinace @Nested tříd a názvů metod s mezerami se toho ale dosáhne velmi elegantně. Takže tento kód:

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureMockMvc
class BlackListRecordControllerTest {  
    ...  
    @Nested  
    inner class `find by id` {          

        @Test   
        fun `should return one record`() {...}     

        @Test   
        fun `shouldn't be found`() {...}     

        @Test    
        fun `should be unauthorized`() {...}  
    }   

    @Nested  
    inner class `find all records` {...}   

    @Nested  
    inner class `create record` {...}   

    @Nested  
    inner class `update blackListRecord` {...}   

    @Nested  
    inner class `delete blackListRecord` {...}  
    ...
}

 

Pak v IDEI (programuje někdo v něčem jiným..?) vypadá po spuštění takto. Za mě dobrý.

 

 

 

 

 

 

 

„Lepší“ 3rd party knihovny

Taky se vám občas stává, že prostě některá knihovna neumí to, co zrovna potřebujete? (Ve Springu většinou ne, Spring má všechno. Jen to občas najít… :)) Nebo to umí, ale…?

Na to má Kotlin extension functions. Prostě si tu chybějící funkci doimplementujete a hotovo. My jsme třeba chtěli mít v testech controllerů defaultně vyřešený POST request z MockMvc objektu. Aby se dalo jednoduše zavolat:

@Test
fun `should return newly created user`() {  
    mockMvc.jsonPostRequest(API_USER_URL, userToSave)    
        .andExpect(...)
}

 

místo této obludy:

@Test
fun `should return newly created user`() {  
    mockMvc    
        .perform(      
            post(API_USER_URL)        
                .with(csrf())        
                .content(userToSave.toJson())        
                .contentType(MedaType.APPLICATION_JSON)    
        )    
        .andExpect(...)
}

 

Což se dá v Kotlinu udělat takto:

fun MockMvc.jsonPostRequest(url: String, dataObject: Any): ResultActions {  
    return this.perform(    
        MockMvcRequestBuilders.post(url)      
            .with(csrf())      
            .content(dataObject.toJson())      
            .contentType(MediaType.APPLICATION_JSON)  
    )
}

 

Ano, i v Javě se na to dá napsat metoda:

ResultActions jsonPostRequest(MockMvc mockMvc, String url, Object dataObject) {...}

 

Ale musí mít o parametr víc a navíc… jak chcete v Javě na Objectu definovat toJson()?

 

Generování dat

Data classes jsou jedna z mých nejoblíbenějších featur Kotlinu. Nemám rád gettery a settery a zbytečnou ukecanost. Lombok @Data zase podle mě rozhodně není elegantní řešení. V testech dost často potřebujeme hodně podobná data. My to většinou řešíme tak, že vytvoříme jeden objekt a ten pro konkrétní testy trošku upravíme. Pomocí copy(), třeba takto:

val baseRecord = Record(  
    validTo = null,  
    incidentNumber = "INC0x001",  
    annotation = "testing annotation"
)
...
@Test
fun `should update`() {  
    val recordToUpdate = baseRecord.copy(id = 3L)  
    ...
}

 

A co dál…?

Null safety je těžce návyková vlastnost. Jednoduše napsatelné (a čitelné) lambdy. Příjemné operace na kolekcích. when, with, ?:…

No, Javu už nechci vidět.

Zobrazit více

Kotlin: The Fun Parts

Kotlin jsme u nás školili i interně. Na materiály se můžete podívat zde.

 

Nadchli jste se pro Kotlin stejně jako my? Pak se můžete těšit na pokračování!