
Kouzlo maker
Proč céčkoví programátoři při příchodu objektově orientovaného programování
museli vymýšlet nové programovací jazyky? Proč kvůli zavedení operátoru
if-then-else v Pythonu probíhaly sáhodlouhé debaty na vývojářském mailing
listu? Proč v Javě musíme neustále opisovat tytéž programové konstrukce?
Odpověď je jednoduchá: V těchto prostředích chybí kvalitní makra.
Co jsou makra?
Každý programovací jazyk disponuje určitou množinou programových konstrukcí,
pomocí kterých lze instruovat počítač k provádění kýžené činnosti.
V mnoha programovacích jazycích existují situace, kdy je užitečné nebo
dokonce nezbytné provádět před překladem kódu jeho přepis. K tomuto
účelu slouží makra, která provádí transformaci kódu ještě před
jeho překladem. Výsledkem aplikace maker na zdrojový kód je opět zdrojový kód
v daném programovacím jazyce.
Člověka mohou napadat otázky jako k čemu je užitečné přepisovat zdrojový
kód a proč na tuto činnost nelze použít obecný makroprocesor. Pokusme se tedy
na problematiku maker podívat trochu podrobněji.
Jazyky bez maker
Některé jazyky nedisponují standardními prostředky pro práci s makry
vůbec. Příkladem takových jazyků může být Python nebo Java. U těchto
jazyků narazíme na překážky již velmi brzy.
Co kdybychom si kupříkladu chtěli definovat nový příkaz until,
který je analogický příkazu while až na to, že řídící podmínku
příkazu neguje? Bez použití run-time kompilace a předávání kódu
v řetězcích nebo zavádění pomocných funkcí pro každou novou podmínku
v until to prostě nejde. Přitom by stačilo mít prostředek na
prostý textový přepis v Pythonu
until -> while not
nebo v Javě
until (condition) -> while (! (condition))
Prosté zavedení funkce until není možné, protože
jí není možno dostatečně jednoduchým způsobem předat podmínku bez jejího
okamžitého vyhodnocení a protože jí lze předat pouze výraz, nikoliv libovolnou
sekvenci příkazů (to je již nedostatek programovacího jazyka). Museli bychom
si pomoci obezličkami, které by celou věc zkomplikovaly a triviální účel
zavedení příkazu until znehodnotily.
Dalším příkladem překážky, k jejímuž překonání chybí makra, může být
nemožnost ošetření absence operátoru if-then-else.
Přestože Python i Java samozřejmě disponují programovou konstrukcí
if-then-else, tato konstrukce není výrazem a tudíž nemůže vracet
hodnotu. Potřebujeme-li vyhodnocovat podmíněné výrazy, musíme si pomáhat
pomocnými proměnnými a chtě nechtě tak patřičně
navýšit počet řádků kódu svého programu. Co bychom například v jazyce C
zapsali jednoduše jako
some_function (x >= 0 ? x : -x)
musíme v Javě (pokud by na konkrétní operaci neexistovala knihovní funkce)
zapsat jako
{
int abs_x;
if (x >= 0)
abs_x = x;
else
abs_x = -x;
some_function (abs_x);
}
Zkušený programátor toto obvykle nedělá a zřídí si raději pro výpočet
použitého podmíněného výrazu pomocnou funkci. Tím se ale nadbytečné řádky
programu jen přesunou jinam. Jak si zřizování pomocné funkce pro každou,
v programu byť jen jedinkrát použitou, programovou konstrukci ušetřit?
Logickým opatřením by byla definice funkce if_:
Object if_ (bool condition, Object then_, Object else_)
{
Object result;
if (condition)
result = then_;
else
result = else_;
return result;
}
abychom mohli psát
some_function ((Integer) if_ (x >= 0, x, -x));
Nutnost přetypování výsledku if_ sice trochu ztrácí eleganci,
omezení typů argumentů if_ na objekty a jejich chybějící typové
kontroly již působí určité potíže,
nicméně nejpodstatnějším problémem je něco jiného. Při takovém
if_ totiž dochází během předávání argumentů k vyhodnocení
obou částí podmínky, nezávisle na tom, která z nich se ve skutečnosti
nakonec použije. Počítá se tak zbytečně i to, co by se počítač nemuselo,
ba hůře, někdy takový podmínkový výraz nefunguje vůbec:
if_ (x == 0, null, 1/x);
Uvedenou funkcí if_ dělení nulou neošetříme, musí nastoupit
transformace zdrojového kódu. Tím jsme v podobných bezmakrových jazycích
v koncích a nezbývá než aby kód programu bobtnal a bobtnal způsoby
naznačenými výše.
Nyní je zřejmé, proč je potřeba při zavádění operátoru
if-then-else v Pythonu debatovat o jeho syntaxi, místo
aby se triviálně zadefinoval. A proč programátoři v Javě skřípou
zubama, když již posté píšou
{
int result;
if ( ...
C preprocesor
Patrně nejznámějším makroprocesorem spjatým s konkrétním programovacím
jazykem je cpp, kterým je standardně vybaven programovací
jazyk C. S jeho využitím je v C zavedení příkazu until
zcela bezproblémové:
#define until(condition) while (! (condition))
To je možné proto, že cpp provádí substituci zdrojového kódu,
tj. při "vyhodnocování" podmínky nedochází k vyhodnocování výrazu.
Definice makra představuje jen prosté zestručnění programového zápisu,
což je to, co obvykle programátor chce a k čemu makra stejně jako
ostatní programátorské prostředky slouží.
S if_ však již opět narazíme, protože se neobejdeme bez
použití vestavěného operátoru ?:. Kdybychom tento operátor
neměli, cpp by nám nepomohlo. Lze sice zestručnit alespoň podmíněné přiřazení
#define if_(result_var, condition, then_, else_) \
{\
int __tmp;\
if (condition)\
__tmp = (then_);\
else\
__tmp = (else_);\
result_var = __tmp;\
}
a psát
if_ (y, x == 0, INFINITY, 1/x);
ne však již něco jako
some_function (if_ (_, x >= 0, x, -x));
protože v argumentu volání funkce nemůžeme použít bloky uzavřené
v složených závorkách. To je ovšem zatím více nedostatek jazyka C než
cpp.
Hlavním nedostatkem samotného cpp je, že neumí provádět libovolný přepis a není
plně integrováno s C. To se projeví například v situaci, kdy bychom
v C chtěli zavést programovou konstrukci with známou
z Pascalu, která umožňuje stručnější přístup k položkám struktury,
přímo jejich jmény, bez uvádění jména proměnné struktury. cpp nemá žádnou
možnost zjistit, jaké členy daná struktura má. Jiným projevem téhož je
nemožnost provádět v cpp složitější operace, takže po něm například nelze chtít
věci jako určování statického seznamu předků třídy, pokud bychom si rádi
makry pomohli při konstrukci nějakého systému objektově orientovaného
programování v C.
A proto C podporu pro objektově orientované programování nemá. Objektově
orientované programování v C sice provozovat lze, avšak jen přespříliš
ukecaně. Potřebná zestručnění jsou totiž, s ohledem na omezené možnosti
cpp, nemožná.
K čemu jsou dobrá makra?
Jak jsme viděli, makra mohou být nezbytná pro definování nových programových
konstrukcí. Různé programovací jazyky je mohou využívat pro různé účely.
V obecnosti lze makra slušným způsobem typicky využít v následujících
situacích:
-
Transformace argumentů. Pokud by v C nebyl příkaz
for a
chtěli bychom ho definovat v obvyklé syntaxi, jednalo by se o funkci
jediného argumentu se třemi složkami odděleným středníkem. Takový argument je
nutno ošetřit speciálním postupem.
-
Předávání identifikátorů. Potřebujeme-li kód, který pracuje přímo
s identifikátory, například zmíněné
with, musíme použít
makro, neboť funkcím lze předávat jen hodnoty.
-
Podmíněné vyhodnocení argumentů. Jedná se o uváděné
if_, kdy
chceme podmíněně vyhodnotit jen některý z argumentů, v závislosti na
podmínce.
-
Opakované vyhodnocení argumentů. Příkladem může být
until, kdy je
předanou podmínku nutno vyhodnocovat opakovaně, ne právě jednou, jak by se
stalo při předání argumentu funkci.
Jednou z oblíbených aplikací maker v jazyce C je také podmíněný
překlad. Můžeme například psát
#ifdef HAVE_SPECIAL_FOO
x = special_foo (y);
#else
x = foo (y);
#endif
a použít tak potenciálně dostupnou vylepšenou funkci. Tato aplikace
maker je diskutabilní, protože k ní makra vlastně nepotřebujeme:
if (HAVE_SPECIAL_FOO)
x = special_foo (y);
else
x = foo (y);
Solidní překladač by měl konstantní výraz HAVE_SPECIAL_FOO
vyhodnotit již v době překladu a nepoužitý kód vůbec nezpracovávat.
Drobná potíž je ovšem v tom, že solidní překladač by vás měl při té
příležitosti na zahozený kód navíc upozornit, což v daném případě není to
co chceme. Chceme-li se tedy zde obejít bez maker, musíme se obejít i bez
kompletního komfortu vývojového prostředí.
Ještě dodejme, jaké mohou být méně slušné aplikace maker. Nejtypičtější
z nich je inlinování kódu. Tuto věc má zvládnout kvalitní překladač,
a proto bychom neměli psát věci jako
#define MY_CONSTANT 123
místo
const int my_constant = 123;
nebo
#define abs(x) ((x) >= 0 ? (x) : (-(x)))
místo
int abs (x)
{
return (x >= 0 ? x : -x);
}
přestože konkrétně v jazyce C může mít definice makra abs
určité opodstatnění s ohledem na jinak těžkopádnou práci s číselnými
typy.
Dalším méně slušným použitím maker je provedení výpočtu v době kompilace.
Například můžeme mít funkci počítající součet druhých mocnin svých argumentů,
které jsou konstantami známými v době překladu. Méně kvalitní překladač
může výpočet odložit do doby běhu programu, zatímco pokud je součet počítán
makrem, je vynuceno jeho provedení již v době expanze makra, tj. před
překladem programu.
A konečně může jít o využívání smluveného identifikátoru. Pokud
například na mnoha místech používáme proměné nazvané connection a
result a často s nimi provádíme nějakou operaci, můžeme si
definovat
#define op_connection result = op (connection)
a potom místo
result = op (connection);
stručněji psát
op_connection;
Takové programové konstrukce jistě nejsou příliš vhodné k obecnému
užívání, ale občas se mohou hodit.
Proč ne obecný preprocesor?
Díky tomu že se při aplikaci maker často jedná jen o textový přepis kódu,
mohli bychom se v jazycích, které makra zcela postrádají, pokusit použít
nějaký obecný makroprocesor, například m4. V řadě situací by to
fungovalo a mnohdy může jít o postačující řešení, leč ve vší obecnosti se
potížím nevyhneme. Makroprocesor neznalý syntaxe konkrétního programovacího
jazyka se může dopouštět nepříjemných omylů. Například mu nemusí být jasný
rozdíl mezi until v programovém kódu a until
v anglickém textu tištěném na výstup. Pak se dočkáme makroexpanze na
místech, kde k ní rozhodně docházet nemá.
Kromě toho není příliš elegantní, je-li syntaxe makroprocesoru odlišná od
syntaxe jazyka. Má-li programovací jazyk rozumnou syntaxi, lze místo
textové substituce mnohem čistěji používat substituci
programových konstrukcí za jiné programové konstrukce, přičemž
makro není nic jiného než malý program v tomtéž programovacím jazyce. Pak
makra s programovacím jazykem naprosto splynou a není třeba se učit jazyky
dva. cpp je v tomto směru polovičaté, m4 již zcela od jazyka oddělené.
Z těchto důvodů je žádoucí, aby makroprocesor byl přímo součástí daného
programovacího jazyka a postupy přepisu maker byly zapisovány také přímo
v tomto jazyce.
Read macros
Makry zhýčkaný programátor může zajít ještě dále a požadovat komplikovanější
formy zápisu. Představme si, že máme program, který velmi často pracuje
s dvojicemi. Rádi bychom je zapisovali co nejstručnějším způsobem a
přitom bychom se nechtěli vázat na jejich nějakou konkrétní implementaci.
Takže nám nevyhovuje ani zápis typu
Pair (x, y)
který je příliš upovídaný, ani třeba zápis pomocí seznamů, který by vyžadoval
implementaci dvojic právě prostřednictvím seznamů. Místo toho by se nám líbil
třeba zápis pomocí hranatých závorek
[x, y]
samozřejmě za předpokladu, že hranaté závorky již nejsou využity jinak.
V dosažení kýženého nám pomohou tzv. read macros. Ta umožňují definovat,
že pokud parser programového kódu narazí na symbol [ (nebo
kterýkoliv jiný, pro který definujeme nějaké read macro), má zavolat funkci
danému read macro programátorem přiřazenou,
která zpracuje následující libovolnou část vstupu a vrátí odpovídající
konstrukci v normální podobě programovacího jazyka. Pro náš příklad
dvojic zapisovaných pomocí hranatých závorek by ona čtecí funkce po svém
vyvolání mohla rekurzivně zavolat parser pro načtení argumentu x,
načetla by čárku, opět zavolala parser pro načtení argumentu y,
zkontrolovala uzavírací hranatou závorku a vrátila přepis
Pair (x, y)
Je to vůbec možné?
Čtenář se možná v tuto chvíli ptá, zda výše popsané vůbec někde může
reálně fungovat. Ano, může a funguje. Všechny uvedené požadavky na makra
splňují lispové programovací jazyky. Právě makra jsou, spolu s absencí
rozlišování programových konstrukcí na výrazy, příkazy a unární, binární,
ternární, atd. operátory, příčinou nesmírné obliby Lispu mezi určitou skupinou
programátorů. A z téže příčiny nemá Lisp problémy ani
s operátorem if-then-else ani s doplněním objektově
orientovaného programování bez nutnosti změny či doplnění jazyka. Makra
v Lispu fungují jako jakási kouzelná hůlka, umožňující jazyk proměnit do
mnoha různých a předem netušených forem.
Makra jsou nesporně velmi zajímavým a mocným programátorským nástrojem.
Budu rád, když čtenáři znalí jiných prostředí s plnohodnotnými
makroprocesory nebo s prostředky makra nahrazujícími v diskusi
k článku přispějí příklady takových prostředí a dalšími příklady využití
maker nebo podobných nástrojů.
Na závěr poznamenejme, že příklady maker uváděné v článku jsou triviální.
Skutečné smysluplné využití maker často spočívá v definicích velmi
komplikovaných konstrukcí, které mnohdy představují něco jako nový jazyk
v jazyce.
|