Poznatky a triky z ruční minifikace

Ruční minifikace kódu, tzv. code golf, je oddychovou programátorskou disciplínou. Cílem je minimalizovat kód tak, aby požadované funkcionality dosáhl na co nejméně znaků – samozřejmě na úkor čitelnosti a robustnosti. Co všechno se při takové činnosti můžeme naučit?

Počátkem roku jsem se rozhodl code golf vyzkoušet na malé úloze, jejíž požadavky vypadaly zhruba takto:

  1. Realizace v HTML+JS+CSS
  2. Objem CSS není podstatný
  3. Objem HTML a JS nesmí překročit 256 bajtů
  4. Výstupem bude mapa s interaktivní postavou
  5. Postava se bude pohybovat po neomezeně velkém prostoru
  6. Aplikace bude obsahovat detekci kolizí

Požadavek na neomezenou velikost je samozřejmě nutné brát s rezervou – hlavní myšlenka je, že data mapy nebudou uložena v paměti, ale dynamicky a deterministicky generována.

Zadání se nakonec podařilo splnit, ale cesta nebyla snadná. Pojďme se podívat, jaké zajímavé techniky a vlastnosti použitých jazyků se při tom hodily.

Co z toho vzniklo

infinite-forest

Výsledek je k ozkoušení na adrese http://jsfiddle.net/ondras/ZZAJH/. S ohledem na povahu webu jsfiddle.net je před používáním klávesnice nutné kliknout někam do okna s výslednou aplikací (aby získalo focus); nejlépe do černého prostoru, neboť jednotlivé znaky jsou HTML odkazy a klikání na ně by znovunačetlo stránku. Proč? To zjistíme za malou chvíli analýzou kódu.

Samotné řešení úlohy je celé obsaženo jen v okně pro HTML (vlevo nahoře). Celý kód je ještě pro čitelnost rozepsán v méně obskurní podobě v levém dolním okně, pro snazší pochopení. Nás bude v tomto článku zajímat minifikovaná verze, která má nakonec přesně 255 bajtů:

<body id=b onkeyup=for(u=v=s="",n=589,p=31,c=event.which,c&&(c%2?u=c-38:v=c-39);n--;)
i=n%p-15,j=(n/p|0)-9,k=(i+x-u)/9+19*(j+y-v),c="abcde"[k*(k*p+S)&15]||"a",i|j||
(c=="a"||Q,c="z"),s+="".link(c);x-=u,y-=v,innerHTML=s onload=S=Date.now(x=y=0);b.onkeyup(b)>

Během vývoje vznikla ještě pracovní verze, která sice pro vykreslování používá jen jednu barvu, ale zato splňuje zadání o něco striktněji, neboť jednotlivé znaky jsou generovány JavaScriptem a nikoliv pomocí CSS generated content. Tu lze obhlédnout na adrese http://jsfiddle.net/ondras/ZZAJH/64/.

Několik triků do začátku

Ručně minifikovaný kód vypadá na první pohled hrůzostrašně. Brzy v něm ale dokážeme rozpoznat hlavní strukturu:

  1. Pracujeme s jediným HTML prvkem, totiž <body>. Vystačili bychom i se značkou kratšího názvu (například odstavcem), ale přišli bychom tak o velmi cenný atribut onload.
  2. JavaScriptová logika je umístěna do atributů onload (initializace) a onkeyup (stisk klávesy). Pro uživatelský zážitek by byl vhodnější atribut onkeydown, odpovídající události stisku klávesy, ale jeho jméno je pro nás příliš dlouhé.
  3. V jazyce HTML 5 není nutné hodnoty atributů uvozovat znakem apostrofu či úvozovek, pokud neobsahují mezery. Kód je tedy upraven tak, aby neobsahoval žádné bílé znaky – tím lze kolem obou atributů získat čtyři bajty místa.
  4. Značka <body> dostala též id; to nám dovoluje se na prvek odkazovat z JavaScriptu pomocí jednopísmenné globální proměnné (http://www.2ality.com/2012/08/ids-are-global.html).

Tenhle JavaScript vás v kurzu nenaučí

Atribut onkeyup obsahuje celou logiku vykreslení mapy; při každém stisku klávesy se tedy vše komplet překreslí. Používání těchto atributů (stejně jako celá řada dalších technik v tomto článku) není doporučeno, ale v našem případě účel jasně světí prostředky.

JavaScriptový kód používá pro zmenšení objemu následující, nepříliš zajímavé techniky:

  1. Proměnné nejsou explicitně deklarovány (klíčové slovo var je zde zbytečný luxus) a jsou tedy globální. Ve striktním režimu by ovšem něco takového nebylo povoleno.
  2. Pro oddělování jednotlivých příkazů se namísto tradičního středníku (a znaku nové řádky) často používá operátor čárky. Výhodný je proto, že výrazy spojené čárkou tvoří jediný výraz a můžeme je tak vložit například do těla cyklu bez nutnosti použití složených závorek. Hodit se také může skutečnost, že operátor čárky nabývá hodnoty posledního spojovaného výrazu.
  3. Inicializace proměnných je umístěna do výčtu formálních parametrů funkcí (tj. mezi kulaté závorky). Takovýto kód není nutné od zbytku syntakticky oddělovat (čárkou, středníkem) a ušetříme tak cenné bajty.

Tělo posluchače onkeyup je tvořeno jediným cyklem while. Bude o něco čitelnější, když jeho kód lehce naformátujeme?

for (
    u=v=s="",
    n=589,
    p=31,
    c=event.which,
    c&&(c%2?u=c-38:v=c-39);
    n--;
)
    i=n%p-15,
    j=(n/p|0)-9,
    k=(i+x-u)/9+19*(j+y-v),
    c="abcde"[k*(k*p+S)&15]||"a",
    i|j||(c=="a"||Q,c="z"),
    s+="".link(c);Př
x-=u,
y-=v,
innerHTML=s

Vidíme, že nejprve dojde k inicializaci většiny proměnných. Iteruje se přes proměnnou n (dolů směrem k nule) a po dokončení cyklu se vyrobená data vypíšou.

Význam použitých proměnných

Pojďme se podívat na proměnné, použité v této ukázce.
code golf

  • x a y jsou souřadnice hráče (znak zavináče). Jejich iniciální hodnota je nula (nastavuje se v atributu onload a při stisku kláves se šipkami se mění podle požadovaného směru.
  • u a v je přírůstek souřadnic (1 a -1) hráče během tohoto stisku klávesy.
  • Do řetězce s si nachystáme celou mapu, kterou pak následně zobrazíme přiřazením do innerHTML.
  • n je počet vykreslených znaků a p je délka řádky. Herní plocha má tedy 589/31 = 19 řádek.
  • i a j jsou souřadnice právě vykreslovaného znaku, relativní vůči vykreslované podmnožině celé plochy. Prostřední bod (tam, co stojí hráč) vždy odpovídá hodnotám i=0, j=0.
  • Do proměnné c ukládáme nejprve kód stisknuté klávesy a následně právě vykreslovaný znak.

V rámci inicializace cyklu využijeme globální proměnnou event a z její vlastnosti which získáme kód právě stisklé klávesy. Platí přitom tato definice:

  • Šipka vlevo = 37
  • Šipka vpravo = 39
  • Šipka nahoru = 38
  • Šipka dolů = 40

Odečtením 38 (pro lichá c) či 39 (pro sudá c) tak snadno dostaneme hodnoty dílčího posunu pro proměnné u a v (řádek 6).

Dejte mi velkou mapu

Dle zadání úlohy nemůžeme mapu držet v paměti (také nemáme kdy a kde její obsah dopředu napočítat). Musíme tedy použít takový algoritmus, který nám pro zadanou dvojici absolutních souřadnic vrátí náhodný, ale deterministický znak.

Nejprve nachystáme pomocné proměnné i a j; zápis |0 odpovídá převodu na celé číslo a jde tedy o ořez desetinných míst. Následuje zápis k=(i+x-u)/9+19*(j+y-v): v kulatých závorkách převedeme lokální souřadnice i a j na absolutní a ty potom velmi základním způsobem zkombinujeme v jediné číslo k.

Následuje jedna z nejzajímavějších částí algoritmu, řádek c="abcde"[k*(k*p+S)&15]||"a. Jedná se o triviální generátor šumu, který pro vstupní označení pozice (k) vrátí jeden z pěti dostupných znaků (abcde). Hodnota k může být záporná, proto nemůžeme použít JavaScriptový operátor modulo (%), který pro záporné operandy vrací zápornou hodnotu. Namísto toho vygenerovanou číselnou hodnotu ořízneme do intervalu 0-15 operátorem &. Z dodané palety pěti znaků pak jeden vybereme; pokud bychom v řetězci přistoupili na vyšší index (5-15), vrátíme znak "a", který odpovídá prázdnému místu a je proto nejčastější.

Bystrého pozorovatele by mohlo napadnout, že v paletě znaků je "a" nadbytečně. My však potřebujeme, aby středové pole (k == 0) vždy obsahovalo znak "a" – odpovídá to logickému požadavku, že hráč musí začínat na volném poli.

Přeskočme nyní veselý třináctý řádek (vrátíme se k němu za chvíli) a dokončeme iteraci. V proměnné c nyní máme jeden z šesti znaků "abcdez" a potřebujeme jej vypsat (tj. přidat k řetězci s). Protože však chceme různé znaky vypisovat různou barvou, použijeme trik: namísto každého znaku zavoláme na prázdném řetězci metodu link, které aktuální znak předáme jako parametr. Jedná se o velmi málo známou funkci, která vyrobí HTML odkaz se zadaným atributem href. Obsahem vytvořené mapy tedy nejsou jednotlivé znaky, ale spousta (prázdných) HTML odkazů. Obsah do nich dodáme následně v CSS.

Detekce kolizí a řízení toku kódu

Jedním z klíčových bodů zadání úlohy byla detekce kolizí. Zjednodušeně lze říci, že pohyb, který by vyústil v posun hráče do neprázdného políčka musí být ignorován. Pro naimplementování této logiky můžeme využít tzv. lazy evaluation logických operátorů v JavaScriptu. Zmiňovaný třináctý řádek bychom mohli ekvivalentně rozepsat takto:

if (i|j) {
} else {
    if (c == "a") {
    } else { Q }
    c = "z"
}

Hodnota i|j je nula jen pokud jsou oba operandy nulové (bitové or je zde nutné, neboť obyčejné sčítání by selhalo v případě, že i == -j). Zajímáme se tedy o souřadnici i = j = 0, tedy střed vykreslované plochy a tím pádem pozici hráče. Otestujeme právě zobrazovaný znak; pokud je to "a", jde o prázdné pole a vše je v pořádku. V opačném případě by však došlo ke kolizi hráče s hmotným znakem a my musíme iteraci okamžitě přerušit. K tomu slouží hodnota Q – jde o nedefinovanou proměnnou, přístup k ní způsobí výjimku a posluchač se přestane vykonávat (tj. nic nevykreslíme).

Když kolize nenastane, přepíšeme aktuální znak na "z", což bude zmiňovaný zavináč.

Vizualizace

Po dokončení iterace upravíme proměnné, které drží stav hráče:

x-=u,
y-=v,

Tyto dva řádky obsahují překvapivý trik. Pokud bychom dílčí hodnoty u a v přičetli, chytili bychom se do pasti operátoru sčítání, který pro řetězcové operandy provádí řetězení. Proměnné u a v ale v rámci úspory místa inicializujeme jako prázdné řetězce, takže si na skutečnou aritmetiku musíme dát pozor.

Na závěr posluchače onkeyup nás čeká ještě jedno slušné WTF: do vlastnosti innerHTML můžeme přistoupit rovnou, bez nutnosti jejího uvození tečkou a jménem objektu. Ukazuje se, že v některých inline posluchačích je prohlížeč povinen rozšířit scope ještě o document a následně relevantní HTML prvek. Více si o tomto specifickém chování můžeme přečíst například v implementaci Google Chr JavaScriptuome nebo přímo ve specifikaci HTML (https://html.spec.whatwg.org/multipage/webappapis.html#internal-raw-uncompiled-handler, odstavec Lexical Environment Scope).

Zbytek vizualizace se odehrává ve světě CSS. Pomocí atributových selektorů a generovaného obsahu naplníme jednotlivé prázdné odkazy požadovanými znaky:

a[href=a]::after { content: "."; color: #ca8; }
a[href=b]::after { content: "*"; color: #555; }
a[href=c]::after { content: "#"; color: #933; }
a[href=d]::after { content: "♣"; }
a[href=e]::after { content: "♠"; }
a[href=z]::after { content: "@"; color: #622; }

Aby celé zobrazení mělo smysl, musíme ještě výsledný dlouhý řetězec zalomit vždy po požadovaných 31 znacích. K tomu můžeme využít málo známou CSS délkovou jednotku ch:

body {
    font-family: monospace;
    width: 31.5ch;
}

Hodnota 31.5 je volena záměrně, kvůli zaokrouhlovacím chybám způsobeným vykreslováním písma.

Pár drobností na závěr

Třešničkou na dortu je inicializace celého mechanismu, probíhající v atributu onload:

S=Date.now(x=y=0);b.onkeyup(b)

Nastavíme zde prvotní pozici hráče a taktéž proměnnou S, která hraje roli Random seed a díky ní máme při výpočtu šumu při novém načtení stránky jinou herní mapu. Následně provedeme iniciální vykreslení tak, že zavoláme dříve definovaný posluchač události keyup. Jako falešný objekt event mu předáme libovolný právě dostupný objekt. Tím, že se v něm nikde neobjeví vlastnost which , bude tato situace korektně ošetřena (šestý řádek v první dlouhé ukázce kódu) a hráč se napoprvé nikam neposune.

Tento JavaScriptový masakr v žádném případě neslouží jako návod, jak bychom měli své skripty vytvářet a strukturovat. Ukazuje však, že i tradičně provařený a rozšířený jazyk může skrývat zajímavé a nezvyklé techniky, o kterých bychom se jinak nemuseli vůbec dozvědět.

Tak co, kdo si dá příští kolo JavaScriptového code golfu? :-)

Autor pracuje ve společnosti Seznam na všem, co alespoň trochu souvisí s JavaScriptem. Ve volném čase se mimo jiné zabývá věcmi, které alespoň trochu souvisí s JavaScriptem. O obojím občas tweetuje jako @0ndras.

Komentáře: 9

Přehled komentářů

mirek pěkný!
Robert Ondřej je špička
davidmoravek stackexchange
Jakub Vrána Ještě jeden bajt
Ondřej Žára Re: Ještě jeden bajt
Jakub Vrána Re: Ještě jeden bajt
Juraj Guniš Preklep
Ondřej Žára Re: Preklep
coke
Zdroj: https://www.zdrojak.cz/?p=14745