images/logo.png
SHWB na blogspot  | Uživatel: Nepřihlášen

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

N100100010000100000
standard forEachavg: 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

N100100010000100000
standard forEachavg: 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.

15.03.2017
Přidat názor:
Vyhrazuji si právo libovolný komentář smazat bez udání důvodu. Kritika mi nevadí, ale chci omezit anonymní výkřiky, které nemají s tématem nic společného.
V textu je možné používat HTML tagy a tuto zjednodušenou MarkDown syntaxi
Jméno
Text
Postřehy:
10.11.2019: Arduino 02 - Co koupit?
Když jsem psal první díl o Arduinu a elektrotechnice, tak jsem si myslel, že na letošní Vánoce budu vybírat nějaký vhodný set. Na podzim to sice vypadalo, že nebudu mít čas se něčemu takovému věnovat, teď to zase vypadá, že by chvíle času zbýt mohla. A tak se zatím zabývám alespoň rešerší možností. Napadají mě totiž otázky jako: originální deska nebo klon, jaká verze desky, jen základní deska a 
extravaganza.controverso@seznam.cz: Zdravím, krásný a informacemi nabitý blog. Musím pochválit. Plánuji rozjet undergroundový zin, co by se týkal black matalu, ambientu, satanismu, left hand
Poslední diskuse Postřehy
O programování 06 - Návrhové vzory - síla i slabina Javy
P.S. samozrejme "Context" mel byt "Client" .. To jsem jen narazil na nejak divne pojmenovany diagram.. (Context je samozrejme trosku neco jineho...)
...
David | 25.02.2017
O programování 06 - Návrhové vzory - síla i slabina Javy
To k cemu jsi dosel (tedy implementace LooperRunner + ILoopMethod), tak je ta prava Strategy by GoF :) To co je tam dulezite je ze Context (LooperRunner) je oddeleny od Strategy (ILoopMethod),
...
David | 25.02.2017
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Máš pravdu, to map je tam zbytečné. Odněkud jsem to opsal a nezkontroloval. Tím ovšem trochu padá pointa celého článku.
...
Saha | 14.12.2016
O programování 03 - Přehlednost funkcionálního zápisu v Java 8
Jen takova otazka k tomu druhemu prikladu:
Proc tam tu cast s "map" ktera de facto s prvky toho streamu nic nedela? Nestacilo by
list.stream().reduce(0, Integer::sum);
?
Ja teda moc
...
David P. | 13.12.2016
Paleo na půl - 01 - První tři dny bez mléka
Kvalitní hořké čokolády jsou bez mléka.. :) (a to i ty méně "kvalitní"). Mléko bývá součástí jen těch "sladkých".
...
David | 04.05.2015
Statistiky
Aktualizováno: 17.11.2019
Počet článků/fotek: 1379/13846
(C) Saha - 1990 - 2019 - Verze 1.3.32 - 11.05.2019 - Generated by SHREC 2.216
Veškeré zde uvedené materiály vyjadřují pouze moje soukromé názory (s výjimkou knihy návštěv a diskusí, kam může přispívat kdokoliv), a pokud s nimi někdo nesouhlasí, tak je to jeho problém, nikoliv můj.