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?
Nálepky:
Počátkem roku jsem se rozhodl code golf vyzkoušet na malé úloze, jejíž požadavky vypadaly zhruba takto:
- Realizace v HTML+JS+CSS
- Objem CSS není podstatný
- Objem HTML a JS nesmí překročit 256 bajtů
- Výstupem bude mapa s interaktivní postavou
- Postava se bude pohybovat po neomezeně velkém prostoru
- 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
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:
- 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ý atributonload
. - JavaScriptová logika je umístěna do atributů
onload
(initializace) aonkeyup
(stisk klávesy). Pro uživatelský zážitek by byl vhodnější atributonkeydown
, odpovídající události stisku klávesy, ale jeho jméno je pro nás příliš dlouhé. - 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.
- 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:
- 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.
- 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.
- 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
ay
jsou souřadnice hráče (znak zavináče). Jejich iniciální hodnota je nula (nastavuje se v atributuonload
a při stisku kláves se šipkami se mění podle požadovaného směru.u
av
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 doinnerHTML
. n
je počet vykreslených znaků ap
je délka řádky. Herní plocha má tedy 589/31 = 19 řádek.i
aj
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ámi=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? :-)
díky za pěkný popis řešení
Holt geniální programátor se nezapře. Do Seznamu je ho skoro škoda.
Kdyby si chtel nekdo zahrat, tak tady je spousta peknejch prikladu … http://codegolf.stackexchange.com/
Je to moc hezké, díky za článek. Právě code golf v JavaScriptu taky řešívám, tak jsem si to užil.
Ještě jeden bajt se dá ušetřit přesunem
s+="".link(c)
dovnitřfor
cyklu: http://jsfiddle.net/ZZAJH/86/To je moc pěkná (a i taková konzistentní, protože to vlastně je skutečný cíl každé iterace) úprava, díky. S dovolením jsem z toho udělal base verzi a přidal tě do contributorů.
Díky. Jen už to teď nemá kulatých 255 bajtů…
V druhej ukazke kodu na riadku 14 je podla mna maly preklep.
Opravene:
Ah, pravda, chybička se vloudila.
Zopar tipov pre buducich golfistov :)
https://github.com/jed/140bytes/wiki/Byte-saving-techniques