Tento článek záměrně načasovávám na dobu, kdy vychází
release GA Javy 8. Pozoruji, jak se už cca od Vánoc na internetu začínají množit články typu "Lambda funkce snadno a rychle" nebo "X nových věcí v Javě 8" (oblíbený formát
dzone.com) a počítám s tím, že po 18.3.2014 bude svět Java vývojářů tímto tématem ještě více zaplaven. Proč jsem si tedy vymyslel, že zrovna teď napíšu o
Guavě? Není tato knihovna utilit s příchodem Javy 8 už mrtvá nebo alespoň za zenitem?
Následující zamyšlení možná pomůže se odpovědi na tuto otázku přiblížit. Guavu aktivně používám od verze 7, která byla vydána roku 2011. Vyzkoušel jsem na našem projektu velkou část funkcionality, kterou nabízí. Odebírám maily z issue trackeru. Navrhl jsem
jeden malý enhancement, který byl akceptován. Uspořádal jsem v našem týmu interní školení se zaměřením na to, jak naše dosud používané patterny v kódu psát efektivněji pomocí Guavy. Výrazně mi usnadnila myšlenkový přechod z objektově-imperativního paradigmatu na objektově-funkcionální. Rád bych zde shrnul svůj pohled na její hlavní přednosti a slabiny a na to, jak je případně ovlivní nástup Javy 8.
Co se mi na Guavě líbí
Nabízí nové datové struktury
Každý, kdo navrhuje nějaký algoritmus, dobře ví, jak důležité je zvolit správné a efektivní datové struktury a Guava tým v tomto jistě není výjimkou. Doba, kdy jediný způsob, jak reprezentovat neskalární data, je seznam, je doufám nenávratně pryč a když mi ji někdo připomene např. kódem, který namísto použití
Set prochází v
O(n) seznam, zda v něm neexistuje prvek, který má v úmyslu vložit, vytahám ho za uši. Pokud je pro vás samozřejmé vědět o asymptotické složitosti základních kolekcí, pokud máte v hlavě zažité rozhodování á la např.
tento diagram, pokud často potřebujete struktury jako
Map<Klíč,Set<Hodnota>> nebo máte představu, k čemu je dobrá mapa s unikátními hodnotami, pak si v packagi
com.google.common.collect přijdete na své a multisety, bimapy, tabulky a jiné se stanou cenným pomocníkem ve vaší práci.
Je škoda, že takové třídy nebyly přidány do Javy 8, v níž by byly ještě užitečnější díky streamům.
Minimalizuje objem pomocného kódu na projektu
Nepočítám-li
projekty, u kterých je neexistence takového kódu cílená a zdůvodněná, asi bychom v běžném životě těžko hledali projekt, který by neměl alespoň jednu třídu
Utils,
Helper, apod. V takové třídě se shromažďuje vše, co je příliš obecné v porovnání s doménou řešeného problému, co se často opakuje (v horším případě u čeho vývojář podlehne klamnému dojmu, že se to bude často opakovat :-)) nebo prostě která se jinam nevešla. Napsat takové třídy může být lákavé pro někoho, kdo si
chce zaprogramovat, ale představuje to zátěž pro údržbu a jako komponenta s vysokou mírou znovupoužití přináší zvýšené nároky na kvalitu a zpětnou kompatibilitu. Použitím Guavy se značně zvýší pravděpodobnost, že tyto utility nebude nutné psát resp. že volání stávajícího proprietárního kódu půjde poměrně snadno refactorovat na volání Guavy.
Promyšlený kontrakt utility metod
Na API všech tříd Guavy je vidět, že ho navrhoval někdo, kdo jej opravdu používá a ví, že kód se jednou píše, ale mnohokrát čte. Je patrné, že řešený problém je důkladně zanalyzován a oproti např.
Apache commons je mnohem důsledněji aplikováno objektové paradigma. Klasickým příkladem je třída
Splitter, která rozlišuje vlastnosti procesu dělení řetězce (metody splitteru) a samotný vstup (parametry metody
split), v protikladu ke skoro kombinatorické explozi čtrnácti nejrůznějších
přetížení metod začínajících split... ve třídě StringUtils v Apache commons. Podobně ze srovnání vyjde
CharMatcher a metody
StringUtils.is.... Přetěžování metod se přitom v Guavě nebojí – když uznají za vhodné, udělají klidně 13 přetížení
ImmutableList.of a 6 přetížení
ImmutableMap.of. Tato čísla nejsou od stolu vystřelené Pišvejcovy konstanty, ale jsou podložena statistickými měřeními v codebase Googlu. Hojně se používá fluent API (řetězení metod). Názvy metod jsou akční, s důrazem na maximálně vypovídající popis činnosti metody (srovnej např. pojmenování
Strings.nullToEmpty s ekvivalentním
StringUtils.defaultString v Apache commons). To mi mimochodem připomíná, že až na název metody
of a pár dalších specifických míst se dá ve většině případů použít statický import metody, a to bez velké újmy na čitelnosti.
Tento přístup je příkladný i pro práci na vlastním projektu. Jeho nejčastějšími opaky, kterých bývám svědkem, jsou: nedostatek úsilí věnovaného výstižnému pojmenování, použití příliš abstraktních termínů náchylných k tomu, že si pod nimi každý představí něco jiného, a děravý návrh meziobjektové komunikace (přílišná závislost na interním stavu, tichý předpoklad volání metod v určitém pořadí nebo volání metody předka apod.). Podívejte se např. na
diskusi (včetně
Google+), která se vedla o to, zda se u stopek pro měření času má nahradit volání
new Stopwatch().start() za factory metodu
Stopwatch.createStarted(), protože je tak z názvu víc zřejmé, zda jsou stopky spuštěné a eliminuje se nejasnost, v jakém stavu jsou po skončení konstruktoru. Kolik programátorů by nad tím mávlo rukou?
Oracle si je u Javy jistě vědom této nutnosti udělat API co nejpříjemnější a nejlogičtější. Situaci docela vystihl
Marcus Lagergren – speaker z Oraclu na Geeconu 2013, kterého jsem se na toto téma – zda si Java 8 vezme v tomto inspiraci z úspěšných opensource projektů – ptal, a jehož odpověď zněla "We inspired from everything." Pokud se mám držet výše uvedených ukázek, v Javě 8 teď nacházíme jak jednoduché metody jako
String.join nebo
Objects.toString připomínající stavbou a pojmenováním Apache commons, tak složitější (byť oproti Guavě méně všestranné) konstrukce jako
StringJoiner. Dlužno připomenout, že Java 8 měla pro přidání metod o jednu motivaci navíc: použití jako
method reference. Je taky férové dodat, že je dobře, že se Oracle nesoustředil na tyto titěrnosti, ale na věci podstatné a principielní. Takže máme
Optional, které nahrazuje guavácké
Optional,
CompletableFuture zhruba nahrazující guavácké
ListenableFuture a především streamy. Při zamhouření oka si lze představit, že aplikace streamů na výsledek
String.split nebo použití
Pattern.splitAsStream bude srovnatelně čitelné s použitím guaváckého
Splitteru.
Realistický přístup k funkcionálnímu programování
Funkcionální paradigma za poslední roky prokázalo, že si zaslouží návrat na scénu. Dokládá to množství nových funkcionálních jazyků a funkcionálních rysů do existujících jazyků včetně Javy 8. Nechci zde rozvíjet úvahy, zda to je či není pozdě. V duchu výroku "Opravdový programátor umí psát FORTRANské programy v kterémkoliv jazyce." ze
slavného článku z roku 1983 je spíš zajímavé sledovat, jak se dá funkcionálně programovat v Javě před verzí 8. Nic moc, že? Podpora v jazyce vesměs žádná, zápis anonymních tříd zdlouhavý. Tak si něco napíšu, řekne si autor budoucí knihovny. Tady je právě ta past: tato oblast není zas až tak těžká na naprogramování – napsat interface
Predicate nebo
Function a kód, který pomocí nich prochází generickou kolekci a transformuje/filtruje/redukuje její prvky, s trochou zkušenosti celkem jde. To, co snahu o překlenutí absence funkcionálních rysů dělá úspěšnou, jsou zde více než jinde
netechnické aspekty. Nestačí dát jen nástroj, ale i jakousi filozofii, jak s ním mají uživatelé nakládat a přitom si zachovat zdravý odstup s vědomím, že to, co používají, je v podstatě jen workaround, protože samotný jazyk nám, co si máme povídat, nepomáhá.
Tím si vysvětluji, proč se vyrojilo tolik knihoven pro podporu funkcionálního programování v Javě, z nichž většina je téměř neznámá: Apache Commons, Functional Java, FunctionalJ, LambdaJ, Jambda, Totally lazy, Bolts, Fun4J, nadstavba nad Guavou Fugue... Nechci, aby následující text vyzněl cynicky, vážím si práce všech, kteří produkují opensource software, ale většina knihoven nabízí v podstatě jen vlastní funkcionální interfacy (které se mezi knihovnami liší jen pojmenováním) a nad nimi nějakou sadu utility metod. Ta jde mimochodem někdy pěkně do šířky (osobně si neumím představit, kdy by se mi vyplatilo např. použít
toto místo klasické konstrukce
switch). Známější knihovny pak použivají chytřejší triky, jejichž výhod i úskalí jsou si předpokládám jejich uživatelé vědomi: proxy objekty (LambdaJ) nebo preprocessing nějaké čitelnější syntaxe.
Filozofie, kterou Guava ve funkcionálním programování razí, je: "dobře, tady je funkcionální podpora, ale je to nutné zlo, jazyk na to stejně není vybaven", případně "nepřeplácejte to s anonymními třídami, nesnažte se o one-liner za každou cenu" (
slušné znění). V issue trackeru resp. na StackOverflow lze najít zoufalé příspěvky typu "jak se mám podrbat levou nohou za pravým uchem" – rozuměj jak zřetězit šňůru anonymních predikátů a funkcí k dosažení požadované transformace dat – následované trpělivou reakcí někoho z Guava týmu typu "použij obyčejný for/if". Přiznávám, že jsem si tímto obdobím taky prošel, vyléčil se z něj a o to víc přístup Guavy vnímám jako pokorný a pravdivý.
Java 8 tento realistický přístup zachovává, vždyť tělo lambda funkce může být imperativní blok kódu. Je tedy trochu paradoxem, že ten, kdo dosud programoval imperativně nebo pouze pomocí anonymních tříd, bude mít z přechodu větší dojem zlepšení než ten, kdo využíval složitější konstrukce. Např. pro výběr všech osob ve věku 36 let je přechod z anonymního predikátu
Iterables.filter(personSet, new Predicate<Person>() {
public boolean apply(Person p) {
return p.getAge() == 36;
}
}
na lambda funkci
personSet.stream().filter(p -> p.getAge() == 36)
poměrně značná úspora, zatímco při realizaci pomocí
Function<Person,Integer> ageFunction = new Function<Person,Integer>() {
public Integer apply(Person p) {
return p.getAge();
}
}
Iterables.filter(personSet, Predicates.compose(Predicates.equalTo(30),Person.ageFunction));
jen ušetříme funkci díky referenci na metodu, ale vlastní kód se o tolik nezjednoduší:
personSet.stream().filter(Predicates.compose(Predicates.isEqual(30),Person::getAge));
Jsem velmi zvědavý, jaký bude další život funkcionálních rysů v Guavě. Určitě tam ještě nějakou dobu budou – cílovou skupinou současné Guavy jsou uživatelé Javy 5 až 7. Nicméně nedivil bych se, kdyby je jednou zcela vyhodili. Díky tomu, že lambda funkce je kompatibilní s každým funkcionálním interfacem, s jehož jedinou abstraktní metodou se shoduje v typech parametrů a návratové hodnoty, by tento přechod mohl být celkem pohodlný pro uživatele nejen Guavy, ale všech ostatních výše vyjmenovaných funkcionálních knihoven.
Symptomy živého opensource projektu
Guava je knihovna Googlu, není proto překvapením, že je hostována na
Googlecode a ne třeba na Githubu. Na oficiální site projektu je vidět, že nejsou zanedbány žádné vlastnosti dobrého opensource projektu. Cenným zdrojem informací je wiki návod – soubor stránek s názvem končícím ...Explained. Jednoduchý issue tracker se srozumitelnými stavy issues, vyznačením, zda je příspěvek od člena Guava týmu, a flexibilním avšak nepřeplácaným vyhledávačem. Pozitivní přijetí komunitou, komunikace přes
StackOverflow i
Google+.
Nechci se v tomto ohledu pouštět do porovnávání Guavy a Javy, jedna knihovna a celá platforma jsou zkrátka nesrovnatelné. Přesto mi přijde, že porovnávat s Javou knihovnu typu Guava je přece jen o malinko menší blbost než jinou knihovnu. Guava cílí na to být obecně (znovu)použitelná a univerzální. Neuškodí jí ani to, že je spojovaná s velkou společností (mám pochopení, pokud to někomu dává dobrý pocit, ale reálně nemám tušení, do jaké míry je to v Googlu aktivita nadšenců a do jaké míry to má politickou podporu). Java jako platforma má samozřejmě taky zvládnuté věci vyjmenované v předchozím odstavci, je to ale přece jen větší kolos. Zatímco nástroje typu Googlecode a způsob, jakým je dobré opensource projekty využívají, jsou pro mne intuitivní a rychle pochopitelné, v protikladu k tomu musím přiznat, že procesu JCP a způsobu, jakým se tvoří a vyřizují JSR a JEP, rozumím jen velmi povrchně. Nepůsobí na mne "nízkoprahově", nemám z něj dojem otevřených dveří.
Pružné zacházení s novými nebo odstavenými featurami
"Compatibility matters" – toto motto si pamatujeme ještě z doby Sunu. Bylo jedním z příčin úspěchu Javy, protože sázelo na lidskou nechuť měnit něco, co funguje. Díky němu si i dnes mohu být teoreticky jistý, že mi můj úžasný program, který projde data v Dictionary, zobrazí je pomocí GridBagLayoutu a pak se ukončí pomocí Thread.stop(), stále poběží. (Čtenář jistě pochopí moji lenost doplňovat k těmto třídám živé odkazy :-)). Guava tuto zátěž vytvářet nechce. Deprecované API (když už nějaké je) je odstraněno 18 měsíců od označení
@Deprecated. Neustálené nebo experimentální API je označeno anotací
@Beta, která představuje jakousi výstrahu "používat na vlastní nebezpečí". Je vidět, že Guava vznikla v
prostředí, kde kód hodně žije, a proto i od projektů, které ji používají, očekává jistou tvárnost. Specifickým fenoménem, který do tohoto bodu řadím, je
hřbitov nápadů – seznam featur, o kterých se autoři zařekli, že tam nikdy nebudou a přes to nejede vlak.
I když ani v tomto aspektu nelze Guavu s Javou porovnávat, vnímám pozitivně jak dynamičnost Guavy, tak konzervativní postoj Javy.
Co mi na Guavě vadí
Funkcionálních rysů mohlo být přece jen víc
Ne zas o moc, abych si neprotiřečil výše napsané oslavné ódě :-). Ale často postrádám vše, co souvisí s prací s dvojicemi: Pair, operace fold, BiFunction, případně transformace známá z Pythonu pod názvem
zip. Také
FluentIterable (analogie sekvenčního streamu z Javy 8) bylo přidáno až ve verzi 12, přičemž ostatní fluent kolekce a mapy zatím nejsou a je docela možné, že teď už se s nimi dělat nebudou. Totéž platí zřejmě i pro nedostatek pomocných funkcí a predikátů, např. by se hodilo cosi jako
Collections.sizeFunction nebo
Strings.lengthFunction. To ale Java 8 aspoň vyřešila mechanismem "method references".
Open source verze je podmnožina interní knihovny Googlu
Tento bod nepatří v pravém smyslu do výčtu negativ. Pokud chcete používat Guavu, počítejte s tím, že v issue trackeru můžete narazit na
řadu připomínek, na které Guava tým reaguje: "jo, to už používáme interně, možná to někdy releasnem", takže aktuálně je jediná možnost si to sám naprogramovat. O důvodech můžeme asi jen spekulovat, ale myslím, že nejčastějšími důvody jsou nevyjasněnost, zda požadavek zapadá do filozofie Guavy, a nedostatek statistických měření z codebase.
Případně (mám ten dojem např.
odtud – všimněte si datumu) i strategicko-obchodní důvody a snaha nedělat knihovnu zas úplně dokonalou – Google je komerční firma a nic opensourcovat nemusel; pokud tak udělal, jsem za to jedině rád a rozhodně tento bod nemíním jako výtku.
Odmítání a váhavost u některých požadavků
O přidání nějaké featury do Guavy se rozhoduje podle principu
utility times ubiquity. To znamená, že featura má šanci se do Guavy dostat, pokud je v porovnání s implementací bez Guavy významným přínosem v oblasti množství napsaného kódu, čitelnosti nebo výkonu (utility) a pokud pro ni existuje množství use casů (ubiquity). Zaručený způsob, jak dostat zamítnutí požadavku, je napsat, že by to v Guavě mělo být jen tak pro úplnost. Na spoustě požadavcích v issue trackeru je sice vidět, že mnoho uživatelů o tomto pravidlu neví a dostávají reakci zaslouženě, nicméně v některých případech ho Guava tým dodržuje až příliš křečovitě.
Příkladem je třeba chybějící přetížení metody
Iterables.getFirst() bez defaultní hodnoty (oficiálně doporučeným workaroundem je prosté volání
iterator().next()), zatímco
Iterables.getLast() má přetížení s defaultní hodnotou i bez ní. Domnívám se, že ustoupit z principu ortogonality ve prospěch principu jednotnosti a abstrakce by v tomto případě bylo lepší.
Dalším jinak správným postojem, který bývá v případě Guavy doveden do extrému, je dlouhé promýšlení nového požadavku. Uživatel pošle návrh na přidání funkcionality, někdo z Guava týmu mu odpoví, že je to specifický případ něčeho obecnějšího, issue se přepne do stavu Research a začne běžet čas. K dnešnímu dni to je
190 požadavků, přičemž polovina z nich je starší dvou let. Jak jsem psal v minulém bodu, stavím se ke Guavě s respektem a vděčností. Rozhodně si nepředstavuji, že se celou tu dobu tým radí, jak vyvinout něco úplně dokonalého, mají své práce dost. Je potřeba s tím prostě jen počítat. Dlužno dodat, že v případě, že se dočkám, to pak stojí za to.
Nejvíce mi chybí přidání
fluent kolekcí a map, kde mi na současném stavu vadí nepřehlednost, s jakou se do sebe vnořují volání
Maps.transformValues(Maps.filterValues(map,predicate),function). Domnívám se, že přechodem na fluent zápis by zde byl princip
utility times ubiquity krásně splněn. Podobných případů se ale mezi požadavky ve stavu Research dá najít víc.
O filozofii Guavy taky vypovídá věta "
když to nepotřebuje nikdo v Googlu, potřebuje to vůbec někdo?". I když hlavním kritériem pro
ubiquity jsou měření na codebase Googlu, při reakci na nový požadavek od někoho zvenčí se Guava tým vždy silně zajímá o realistický, praktický use case. V naprosté většině případů se jedná o dobrou radu od zkušeného programátora, ale je potřeba mít na paměti, že co je dobré pro Google, nemusí být dobré i pro mne.
Každý bod končí přenesením příslušné úvahy na Javu 8 a zde to bude triviální: na ni jsme čekali dlouho a teď jsme se dočkali :-).
Monolitický jar
Guava se distribuuje jako jeden jar, který v aktuální verzi 16 má 2.1 MB.
Pro řadu uživatelů už to je moc, nicméně
oficiálně doporučovaný workaround je poměrně nestandardní – použití nástroje ProGuard. Na našem projektu je součástí klientské aplikace stahované přes Java Web Start. Vzhledem k ostatním okolnostem je to zatím únosné, přesto se moc netěším na dobu, kdy nastane potřeba tento stav změnit. Bude to pak zřejmě rozhodování, zda se pustit do konfigurace ProGuardu a zásahu do build procesu anebo – alternativně – zda nesáhnout po open source projektu
Seeds, což je Guava rozdělená do několika jarů (funkčně shodné, pouze je změněn název hlavního package) dobrovolníkem
Jesusem Zazuetou, kterému zřejmě došla trpělivost a udělal analýzu závislostí a rozdělení na submoduly sám (odkazy na nové porty Seeds pro nové releasy Guavy posílá jako komentáře do issue trackeru).
Návod na použití ProGuardu je poměrně vágní, což mi moc nesedí dohromady s precizností ostatních návodů. Vzhledem k tomu, že končí větou "We would like to hear about your experiences using ProGuard with Guava so we can improve this page.", poslední editace stránky je z května 2012 a uživatelská komunita je jinak poměrně živá, vyvozuji z toho, že komunita tento bod filozofie Guavy nepřijímá za vlastní a podobně jako my to vždycky radši nějak překousnou s velkým jarem.
Touto poslední připomínkou ke Guavě se zároveň loučím i s Javou 8, protože očekávám, že popisovaný problém bude vyřešen v Javě 9 – projektu
Jigsaw. Ale o tom si povíme zas za 2 roky :-).
Závěr
Domnívám se, že Guava bude mít stále smysl pro Javu 5 až 7, ale v zeštíhlené podobě i pro Javu 8. Guava je mi dobrou pomůckou při plnění programátorských úkolů, kde je nežádoucí přílišné odvádění pozornosti k implementačním detailům. V článku jsem se snažil o prezentaci pragmatického postoje – nejsem fanboy, nenosím ji na tričku a pokud čas ukáže, že je za ni lepší náhrada, nemám nejmenší problém ji postupně opustit. To však nesnižuje uznání, které bych chtěl vyjádřit autorům za to, že taková knihovna existuje a dali ji komunitě k dispozici.
Máte s Guavou taky vyhraněnou zkušenost? Setkali jste se v Guavě s dalšími příklady fenoménů, které jsem popisoval? Budu rád, když se podělíte v komentářích.