Redundance dobrá a zlá

My programátoři z podstaty své legendární lenosti neustále hledáme způsob, jak napsat co nejméně kódu. DRY princip je cosi jako svaté přikázání. Hodně lidí jde ještě dál a snaží se napsat co nejméně znaků a nemusí se to týkat jen délky identifikátorů. Víte třeba o tom, že v Javascriptu je ve většině případů možné vynechat středník? To už může být kontroverzní praktika, protože podvědomě cítíme, že některá úsporná opatření mohou být kontraproduktivní. V jakém rámci to ale můžeme hodnotit? Existuje něco jako “dobrá” redundance?

Jiný příklad diskutabilní redundance jsou i datové typy. Nekonečné zápisy generik všude možně jsou jedním z nejpádnějších argumentů dynamicky/volně typovaných jazyků proti těm striktním, ale tak jednoduché to není. Předně důkazy o největších takových zločinech proti lidskosti totiž pocházejí z jazyků, které jsou hrůzostrašné bez ohledu na jejich striktnost, jako třeba Java. V jazycích s dobrým typovým systémem, inferencí a dalšími vychytávkami (Haskel apod.) však musíte ve skutečnosti deklarovat datové typy jen na velmi málo místech a ještě k tomu velmi úsporně. 1

Ztrácí tedy díky Haskelu diskuze o dynamicky/volně typovaných jazycích smysl? Ne, jelikož přestože Haskel dělá všechno pro to, aby datové typy nebylo potřeba deklarovat, jeho best practice naopak diktuje zapisovat je i na mnoha nepovinných místech. Odkud se bere toto paradoxní doporučení?

Vychází z praktické zkušenosti, že bez jejich důsledné deklarace jsou případné kompilační chyby většinou dost kryptické a vůbec nepomáhají s opravou. Díky generikům, inferenci a spol mohou vadné datové typy cestovat v kódu skrz mnoho úrovní i modulů aniž by byly detekovány a vyplavou na zcela nesouvisejícím místě bez viditelné spojitosti ke kořenové příčině problému.

Vypsat přesný datový typ i na nepovinném místě tak slouží jako hráz proti nekontrolovanému šíření chyby, stejně jako ten středník v Javascriptu. Jednoduchost, flexibilita a tolerance Javascriptu, které tak usnadňují začátečníkům a laikům práci v něm, zároveň dovolují kompilátoru validně interpretovat kód následující po nějaké chybě opravdu dlouho v divokém divnokontextu, který danou chybou vznikl. Pro lidi je neviditelný, protože v hlavě mají mustr správné interpretace a nenapadne je hledat jiný, ale kompilátor nemá v kódu dostatek redundance, aby si mohl všimnout, že spokojeně chroustá znaky v chybovém runaway módu2. Redundance středníku v takových případech je hlavním důvodem doporučení ty středníky psát.

Širší souvislosti

Dobrá redundance v teorii informací je ta, která umožňuje efektivně odhalit chyby při přenosu informace nebo je dokonce rovnou opravit. Tato vlastnost je v teorii kódování studována jako “samodetekující” a “samoopravné” kódy a z hlediska matematiky je možné celkem jednoduše vypočítat minimální redundanci potřebnou k jakýmkoli cílovým prahům detekce i opravy. S kompilátorem však bohudík nekomunikujeme naše myšlenky Hammingovými kódy, ale s pomocí programovacího (tzn. lidského!) jazyka a musíme vynalézt metody redundance i vyhodnotit jejich efektivitu právě v kontextu lidského jazyka a myšlení.

Výše zmíněné příklady spadají spíše do kategorie detekce a mohlo by se proto zdát, že se jedná o poměrně okrajový jev, který nás zajímá až v okamžiku, kdy začneme buildovat projekt a ještě k tomu jen když je v něm chyba. Je potřeba si ale uvědomit, že v každém modernějším IDE ve skutečnosti běží kvůli code completion ekvivalent kompilátoru prakticky neustále a že rozpracovaný kód je z principu mimo pár náhodných vzácných okamžiků neustále rozbitý.

Je potřeba si uvědomit, že i taková prkotina jako červeně podtrhnout správný token obsahující chybu bez toho, aby se červená rozlila i na celý zbytek kódu v souboru a tranzitivně pak do celého projektu, vyžaduje, aby kompilátor odtušil z okolních náznaků vývojářův skutečný záměr. I pro kód s chybou musí intellisense vyprodukovat validní AST ve kterém chybu de facto opraví tak, aby se výsledek co nejvíce podobal pravděpodobnému záměru a tím pádem s sebou stáhl co nejméně okolního závisejícího kódu. Že k tomu navrch přidá ještě návrhy “Quick fix” už je pak jen třešnička na dortu.

Jak zmlsaný jsem schopnostmi současných IDE v tomto směru jsem si uvědomil, když jsem potřeboval editovat složitější Java streamy v Eclipse. Zatímco u normálního kódu funguje error containment i v Eclipse solidně, v desetiřádkovém streamu plném lambd nás většinou čeká jen desetiřádkové červené podtržení a hláška “Unable to infer argument types.” a pak babo raď.

Pouze začátek

Redundance v našich kódech má tedy dobrý důvod a ve skutečnosti jí tam vnášíme záměrně. Jde jen o to, aby bylo jejím dobrým designem docíleno co nejlepšího poměru mezi pracností navíc a přínosem, který to přináší. Primárním zdrojem redundance je syntax jazyka samotná, ale zdá se, že to nestačí a vymýšlíme další.

Oblíbeným východiskem pro to je, podobně jako u podvojného účetnictví a jeho “rozvahy”, vymyslet, jak dvěma způsoby udělat to samé a porovnat výstupy. Na tomto principu stojí datové typy i třeba TDD, přestože jsou jinak velmi odlišné - rozdělují vše na nějaké tvrzení a faktické naplnění onoho tvrzení. Z nějakého důvodu je to u lidí zvlášť oblíbený vzorec myšlení, ale to už je na další samostatný článek.

  1. Java k tomu sice pomalu míří taky, nejdříve skrze inferenci generik, nověji díky inferenci celých proměnných přes var, v blízké budoucnosti snad přijdou i “value classes”, ale ani ostatní jazyky nespí a druhou (třetí?) typovou ligu tak bude Java pravděpodobně hrát i dál. 

  2. Do problematiky runaway dezinterpretace instrukcí v odlišném kontextu nejzábavněji zasvětí desková hra Robo Rally, vřele doporučuju.