29. května 2017

Covariance & Contravariance

Myslím, že je dost pravděpodobné, že na univerzitě jste měli nějaký ten semestr statistiky (já jsem měl 4 😱 ) a tak by vám měl být aspoň lehce povědomý termín kovariance, který je vyjádřený vztahem:
cov(X, Y) = E[(X - E[X])(Y - E[Y])]
Tak o tom si dnes povídat nebudeme.

Místo toho bych si chtěl rozebrat téma, které jsem dostal jako otázku na nedávném pohovoru. (Jo, to byl ten pohovor, kvůli kterému jsem napsal článek CAP Theorem, takže to už je druhý zářez na pažbě.)

Když jsem tedy dostal otázku, jestli vím, co je kovariance a kontravariance, tak u prvního termínu mi zablikala kontrolka "mlhavé vzpomínky" (léta nepoužívaná statistika se projeví "určitým povědomím neurčitosti") a pak jsem na férovku přiznal, že termín kontravariance jsem nikdy neslyšel a nevím, co to je.

Poslední varování! Pokud vás neodradila rovnice v úvodu ani vás neodežene tento odstavec, vězte, že se dozvíte o temném zákoutí computer science, které vám asi bude prakticky k ničemu - já jsem tuto "záležitost" za 12 let v Javě nepoužil ani jednou. Na druhou stranu, je to aspekt, který je ve vašem jazyce trvale přítomen a dost možná jsou vám jeho praktické konsekvence známé.

Podivný případ s Array

Než se do toho pořádně pustíme, zkuste se podívat na následující kód v Javě, který se bez problémů zkompiluje. Co je na tom špatně a co se stane, když ho spustíme?


Asi jste správně odhalili, že na 9. řádku micháme jablka s hruškama. Ehm, tedy Jedie se Sithama. Možná už ale nevíte, že zmíněný řádek vyhodí runtime výjimku ArrayStoreException.

Pokud bychom se podívali na ekvivalentní příklad ve Scale, tak se nám následující kód ani nezkompiluje, protože vyhodí kompilační chybu už na řádku 8.


To jsou věci! Vítejte ve světě variance.

3 sestry: Kovariance, Kontravariance a Invariance

Výše uvedené příklady demonstrují, že pole jsou v Javě kovariantní a ve Scale invariantní. Pojďme si na to posvítit. Všechno se to motá kolem pojmů subtyping (pozor, neplést s dědičností) a variance. Takže.

Variance je obecný pojem, který říká, jakým způsobem funguje subtyping u komplexních typů. Komplexním type je třeba generická kolekce, nebo funkce. Variance může být trojího druhu:
  • Invariance říká, že mezi komplexními typy není žádný vztah.
  • Kovariance umožňuje nahradit komplexní typ jeho podtypem.
  • Kontravariance umožňuje nahradit komplexní typ jeho nadtypem.

Pro úplnost je potřeba říct, že je tady ještě čtvrtá sestra: bivariance. Ale protože ji macecha ráda neměla, tak se s ní nebudeme zabývat.

Variance v kolekci

Pokud se vrátím k výše uvedenému příkladu v Javě - proč je pole kovariantní, když to pak působí problémy? Důvod je historický. Pole byla jedna z prvních kolekcí, které Java měla a jak ti starší z nás zažili, až do verze 5, neměla Java generika. (Mimochodem, jeden z lidí, kteří generiky do Javy přidávali, byl Martin Odersky, autor Scaly.)

Pole v Javě byly navrženy, aby podporovaly pole objektů a protože každá třída automaticky dědí z Object, tak jsou pole kovariantní. S příchodem generik to už nedává smysl a tak máme v Javě takovou dichotomii - pole jsou kovariantní (viz příklad nahoře), kdežto generické kolekce jsou invariantní.

Jak taková invariance v kolekci vypadá? Podívejme se na následující příklad seznamu v Javě:


Teď už by mělo být zřejmé, že ačkoliv typy v kolekci podporují subtyping (např. Jedi -> Powerful), tak kolekce samotné jsou invariantní: seznam List<Jedi> není podtypem List<Powerful>.

Immutable kolekce

Jak zrhuba říká Martin Odersky (tady hodně zjednodušuji), mutable kolekce by měly být invariantní, zatímco immutable kolekce můžou být kovariantní.

Kovariantní seznam si můžeme hezky ukázat ve Scale. Jelikož List je ve Scale immutable (ve skutečnosti je to klasická funkcionální struktura cons buněk). Právě immutabilita nám zajistí, že nás nepotká podobné runtime překvapení, jako u Java polí.


Tam, kde se nám Scala u polí bouřila (řádek 8), tak u seznamu ani necekne. Prostě kovariantní pohodička.

Jakou varianci má immutable Java?

Když se člověk dívá, jakou pěknou implementaci immutable kolekcí má Scala, tak ho napadne, jestli jsou také kovariantní immutable kolekce v Javě. Zklamu vás... nejsou.

V první řadě, immutable kolekce v Javě vůbec nejsou. To nejbližší, co se dá v Java kolekcích najít je Collections.unmodifiableList (ev. Map, Set, Collection atd.), což je ale jenom read-only pohled na interní, mutable list. Který je invariantní.

Pokud se podíváme na externí frameworky, immutable kolekce nabízí Google Guava. Když se na ně ale zaměříme z hlediska variance, tak jsou opět invariantní. Navíc, ošklivost builderu pro přidání elementu do kolekce se s funkcionální krásou cons nedá srovnávat:


To je všechno? Co funkce?

Povídání o varianci v kolekcích se nám trochu protáhlo, takže se nedostalo na to nejzajímavější - variance ve funkcích. Jako opravdu ve funkcích - Java se nám vrabčími krůčky přibližuje funkcionálnímu programování, takže bychom toto téma neměli minout.

Související články

  • Covariance & Contravariance II: Funkce (TBD)

9. května 2017

REST contract-first: Swagger & Gradle

U webových služeb mám rád přístup contract-first. Jsem 100% přesvědčen, že tak vzniká lepší design i lepší API.

V případě SOAP webových služeb je to celkem běžné. (Teda pokud webové služby "nedesignují" programátoři - to pak většinou skončí "vyzvracením" interního kódu na veřejnost.)

Ohledně REST-ových služeb mi to přijde jako minoritní způsob. To je jednak škoda a jednak problém. Ono to nakonec vždycky nějak funguje. Ale jen výjimečně pak vznikají API, které vývojáři milují.

Jak tedy na REST contract-first službu? Následuje popis, který jsem použil na stávajícím projektu a se kterým jsem - po vychytání (Swagger) much - spokojen.

Jak by to mělo fungovat?

Mám dost jasnou představu, jak by celistvý přístup contract-first měl fungovat. Pokud máte jiný postup, nebo se mnou nesouhlasíte, budu rád, když se podělíte v komentářích. Můj zobecněný přístup vypadá takto:
  1. Napsat kontrakt v nějaké rozumně standardizované a obecně přijímané specifikaci.
  2. Vygenerovat potřebný kód, zejména model a rozhraní (API).
  3. Vygenerovaný kód by měl být v adresáři, kde build tool očekává zdrojové kódy.
  4. Generování a kompilace vygenerovaného kódu je součástí standardního build lifecycle (není potřeba je spouštět samostatně).
  5. Ideálně, generování a kompilace probíhá jen tehdy, pokud došlo ke změně specifikace.
  6. Implementaci rozhraní si píšu sám, ručně.

Swagger

Swagger je soubor nástrojů, které se točí kolem OpenAPI specifikace. Za OpenAPI si můžete představit "něco jako WSDL pro REST". Pro naše potřeby nás budou zajímat dva nástroje: Swagger Editor pro napsání specifikace a Swagger Codegen pro generování kódu ze specifikace.

Swagger Editor

Swagger Editor je webový editor, který si můžete vyzkoušet on-line, ale pro reálnou práci bude lepší ho mít lokálně. Je trochu otravné, že kvůli tomu musíte nainstalovat Node.js, ale jinak má lokální verze víc šikovných funkcionalit.

Swagger Editor

Mimochodem, za celým Swaggerem stojí společnost SmartBear, která dělá SoapUI, takže očekávejte něco podobného - je to proklatě použitelné a v detailech mrzce nedotažené. A mizerná dokumentace.

Swagger specifikace

Swagger specifikace může mít dva formáty: JSON, nebo YAML. Jak na zmíněném projektu, tak v článku jsem zvolil YAML - jednak je to čitelnější pro lidi a pak, aspoň na projektu, to nebudou číst jenom programátoři.

Takže, contract-first. Začneme jednoduchou Swagger specifikací, která nám odpoví na otázku Života, Vesmíru a vůbec:


Swagger Codegen

Swagger Codegen je Java knihovna s CLI rozhraním. Swagger se chlubí, že pro generování kódu podporuje 20 serverových a 40 klientských řešení. Moje ukázka je ve Springu, ale klidně si vyberte svoji oblíbenou platformu.

Swagger Codegen CLI

Jak už jsem to naťuknul výše, ne všechno je ve Swaggeru perfektní - kolegové si dost stěžovali na kvalitu a použitelnost generovaných artefaktů pro JAX-RS a pro C#.

Já jsem sice s výsledkem spokojený a dělá to přesně, co jsem chtěl, ale bylo potřeba to poladit - strávil jsem cca 2 dny experimentováním, než jsem se dostal do stavu "akceptováno". Dva dny se mohou zdát hodně, ale jednak to byla zábava a jednak se to v blízké budoucnosti bohatě vrátí.

No, komand-lajna je sice boží, ale my se s ní patlat nebudeme, jsme přece profíci - použijeme Gradle.

Gradle

Pokud na Gradle Plugin Portálu zadáte heslo Swagger, vyjede vám 7 pluginů. Trochu jsem se bál, abych si nemusel napsat vlastní Gradle plugin, jako se mi to stalo u JAX-WS, protože žádný plugin nedělal, co jsem očekával. Ale nakonec po pročtení GitHubu a vyzkoušení jsem vybral plugin org.hidetake.swagger.generator, který šel rozumně ohnout pro moje potřeby.

Konfigurace zmíněného pluginu v build skriptu build.gradle může vypadat následovně (po ořezání ostatních věcí, které nás z hlediska Swaggeru nezajímají).


Podstatné věci na uvedeném skriptu:
  • V závislostech nám přibyla konfigurace swaggerCodegen.
  • Kvůli Swagger anotacím generovaným do API tříd je potřeba přidat závislost na swagger-annotations. To je sice otravný, ale asi nutná daň za použití Swaggeru. Naštěstí má ta knihovna jen 20 kB.
  • Cílová platforma, pro kterou generujeme, je definovaná atributem language.
  • Aby se nám negeneroval veškerý Swagger čurbes, omezíme generované artefakty atributem components, kdy říkáme, že chceme jenom model a api.
  • Adresář s vygenerovanými artefakty je potřeba přidat ke zdrojovým souborům pomocí sourceSets. (To už není plugin, ale čistý Gradle.)
  • A poslední řádek předsadí v lifecyclu task generateSwaggerCode před kompilaci Java kódu.

Bohužel, tím jsme s konfigurací ještě neskončili - tady to plugin mohl dotáhnout ještě dál. Sofistikovanější nastavení je potřeba dotáhnout v souboru config.json:


Tady stojí za zmínku dvě věci:
  • Jednak prázdný atribut sourceFolder, to aby nám Swagger nevygeneroval duplicitně zanořené adresáře.
  • A potom říkáme, že chceme vygenerovat jenom rozhraní (atribut interfaceOnly), aby nám Swagger rovnou negeneroval prázdné dummy kontrolery. (Možná použitelné jako stuby, pokud generujeme jenom jednorázově.)

A je to. Pokud spustíme build příkazem gradle build, dostaneme následující strukturu, kdy Swagger specifikace je v adresáři src/main/swagger a generovaný kód v adresáři src/generated/swagger:

Adresářová struktura projektu se Swagger definicí a generovaným kódem

Ukázkový projekt

Pro potřebu článku jsem vytvořil malý projekt na Bitbucketu, který vygeneruje potřebný kód a jde spustit v embeddovaném Jetty. Stačí naklonovan a spustit gradle jettyRun.

Měli byste ho vyzkoušet - minimálně vám odpoví na nejzákladnější otázku života. A vesmíru. A vůbec...

Související externí články