O programování 10 - Java a inspirace v Ruby
Na závěr předchozího dílu jsem slíbil, že teď bude čas na něco úplně jiného ... a nebude to Python, ale těsně vedle - Ruby.
V předchozích dílech jsem uvedl některé možnosti funkcionálního přístupu v Java 8 a důvody, proč se mi použitý zápis nelíbí, i když jsem byl nakonec nucen uznat jistou logiku a svoje původní názory revidovat. Občas jsem zmínil, že by se mi více líbil jiný zápis, podobný například tomu používanému v Ruby.
I v Ruby se jedná o volání API, nikoliv o rys jazyka, ale přijde mi, že funkce jsou pojmenovány logičtěji. V této kapitole proto uvedu zápis některých dříve uvedených fragmentů v Ruby a to konkrétně ve verzi JRuby 1.7.3. Novější verze mají sice i další a lepší možnosti zápisu, mně se však nechtělo instalovat Ruby jen kvůli jednomu článku.
BTW, kdysi jsem uvažoval o větším programování v Ruby, některé jeho rysy jsou fajn a jiné zase ne; k tomu se vrátím v závěru článku.
Kopírování pole
Prvním příkladem je kopírování jednoho pole do druhého s modifikací jeho hodnot, konkrétně přičtení jedné. To lze napsat v Ruby jednoduše na jeden řádek s jasnou syntaxí:
list_out = list_in.map {|row| row + 1};
V Javě jsem použil a zkoumal různé varianty následujícího typu cyklu:
inputList.stream().forEach((integer) -> {
outputList.add(++integer);
});
Podle mě jde o principiální rozdíl ve způsobu řešení. Zápis v Ruby říká, že se má na každý prvek pole aplikovat (mapovat) funkce, což je funkcionální přístup. Ale abych Javě nekřivdil, stejný přístup mohu použít také:
outputList = inputList.stream().map(e -> ++e).collect(Collectors.toList());
Super, to je fakt pěkný zápis, jenom drobnost, nešlo by to bez toho collect()? A ideálně ve formě:
outputList = inputList.map(e -> ++e);
Takže bychom měli celkem elegantní zápis (i v Javě), ještě mě napadá otázka, zda je garantováno pořadí, v němž jsou prvky zpracovány? Podle mě obecně nemusí být, což je další podstatný rozdíl.
Druhá otázka je, jak je tento kód výkonný? Naváži na druhý díl a doplním tam publikovanou tabulku o třetí řádek, viz tabulka.
WARMUP = 10
N | 100 | 1000 | 10000 | 100000 |
---|---|---|---|---|
standard forEach | avg: 0,017 min: 0 max: 3 | avg 0,086 min: 0 max: 9 | avg: 0,223 min: 0 max: 4 | avg 2,276 min: 1 max: 16 |
stream().forEach() | avg: 0,018 min: 0 max: 4 | avg: 0,049 min: 0 max: 2 | avg: 0,254 min: 0 max: 10 | avg: 1,819 min: 1 max: 23 |
stream().map() | avg: 0,038 min: 0 max: 1 | avg: 0,191 min: 0 max: 16 | avg: 0,906 min: 0 max: 27 | avg: 5,059 min: 2 max: 43 |
WARMUP = 100
N | 100 | 1000 | 10000 | 100000 |
---|---|---|---|---|
standard forEach | avg: 0,009 min: 0 max: 1 | avg: 0,064 min: 0 max: 10 | avg: 0,199 min: 0 max: 4 | avg: 2,023 min: 1 max: 16 |
stream().forEach() | avg: 0,006 min: 0 max: 1 | avg: 0,049 min: 0 max: 5 | avg: 0,190 min: 0 max: 4 | avg: 1,700 min: 0 max: 11 |
stream().map() | avg: 0,042 min: 0 max: 8 | avg: 0,120 min: 0 max: 5 | avg: 0,762 min: 0 max: 21 | avg: 5,375 min: 3 max: 32 |
Závěr je jednoduchý - tento kód je suverénně nejpomalejší.
PS: Neporovnávám rychlost Java vs. Ruby, protože mi to nepřijde fér a navíc si nemyslím, že by to mělo nějakou vypovídací hodnotu.
Sečtení hodnot pole
Druhým příkladem je sečtení všech prvků pole. V tomto případě nabízí Ruby dvě varianty, první je přímočará:
sum = 0;
list_in.each { |row| sum += row};
a druhá o něco sofistikovanější:
sum = list_in.inject(0, :+);
Druhý případ je "jen" syntaktická zkratka pro "folding" funkce a lze zapsat i takto - viz tento článek
array.map(&:to_i).reduce(0, :+)
což odpovídá javovskému:
int sum = list.stream().reduce(0, Integer::sum);
Javovský zápis mi nepřijde ideální, i když má svoji logiku. Metodu stream() už znovu rozebírat nebudu, přidám však další dvě otázky: Proč se metoda, která aplikuje metodu na prvky pole jmenuje reduce() a ne (třeba) apply()? A proč je tam první argument nula? Obojí si sice dovedu vnitřně odůvodnit (degenerovaný map-reduce, iniciální hodnota), ale pořád mi přijde, že Java ve snaze být univerzální dělá jednoduché věci složitě. Zkrátka by se mi v tomto konkrétním případě líbil zápis:
int sum = list.apply(Integer::sum);
To už jsem ale odbočil od Ruby, takže se vrátím zpět a to rovnou k závěru. I když jsem toho zde mnoho neukázal, tak mi přijde, že v Ruby jsou logičtěji pojmenované metody, ale jeho paradigma je stejné jako v Javě - funkcionální rysy jsou implementované jako metody a nejde o rys jazyka. A to, že je zápis v Ruby mnohdy čitelnější a logičtější plyne z jiných jeho vlastností, nikoliv ze způsobu implementace funkcionálních rysů.
Proč ne Ruby
A ještě odpověď na otázku z úvodu, proč jsem se nakonec nerozhodl pro větší programování v Ruby. Hlavním důvodem je dlouhodobá nestabilita, resp. nepředvídatelnost chování. V Ruby může kdokoliv přepsat libovolnou metodu z API (klidně i String.toUpper) a navíc lze třídu zapsat ve více souborech. Takže v jednom souboru lze napsat tři (public) třídy a ve druhém souboru čtvrtou třídu a dvěma již existujícím přidat metodu. Oba tyto rysy jsou fajn, ale mohou vést k hodně zákeřným a těžko odhalitelným chybám.
To, že někdo přepíše knihovní funkci lze (procesně) ošetřit v rámci jednoho (malého) teamu, mnohem horší je, když se to stane v rámci implementace jazyka jako takového nebo nějaké hojně používané knihovny, což kdysi (cíleně) udělali lidi z velmi populární frameworku Ruby on Rails a to v rámci minor verze (plácnu, při přechodu z 1.9.6 na 1.9.7), což by člověk opravdu nečekal. Důsledek byl, že program běžící v samotném Ruby nefungoval v Ruby on Rails správně, protože (měl tu drzost, že) spoléhal na standardní knihovnu. A podobné věci se údajně děly i přímo v Ruby, resp. jeho knihovnách.
Něco takového prostě na mission critical aplikace nenasadím, nebudu riskovat, že mi minor aktualizace rozbije aplikaci. Ona ani Java není v tomto svatá, programátoři v JavaFX by mohli vyprávět o přechodu z 8.0.29 na 8.0.40. Ovšem drobný rozdíl tu je - u Javy, jakožto kompilovaného jazyka nás na tyto chyby upozorní už překladač, takže program ani nepřeložím natož abych ho spustil. U interpretovaného Ruby však nemám žádný způsob, jak tento problém zjistit. Chybu samozřejmě zjistím po spuštění, ale pokud se nedostanu do větve s příslušným kódem (šikovně zašitý else), tak se o ní dlouho nemusím dozvědět, takže jediným řešením jsou důsledné testy. Ale všichni víme, že stoprocentní pokrytí testy je z říše pohádek...
Takže z těchto důvodů je pro mě Ruby sice zajímavá ale pořád jen hračka.
- O programování 01 - Úvod
- O programování 02 - Efektivita funkcionálního přístupu v Java 8
- O programování 03 - Přehlednost funkcionálního zápisu v Java 8
- O programování 04 - Java Microbenchmark Harness
- O programování 05 - Další varianty Javovské implementace foreach
- O programování 06 - Návrhové vzory - síla i slabina Javy
- O programování 07 - Funkce jako argument aneb od factory k lambdě v Javě
- O programování 08 - Jak v Javě předat metodu, která bude mít různé parametry
- O programování 09 - Pokročilé využití streamů a funkcionálního přístupu v Java 8
- O programování 11 - Minimalistické programy a Java
- O programování 12 - Paralelní implementace násobení matic v Java - úvod