Metaprogramovanie

Čo je metaprogramovanie?

Pri programovaní vytvárame programy, ktoré manipulujú s nejakými dátami — dostávajú dáta z nejakého zdroja, transformujú ich, robia na nich nejaké výpočty, alebo generujú dáta. Program samotný, jeho kód, štruktúra a stav vykonávania, to sú tiež dáta. Preto je možné napísať program, ktorý manipuluje programami. Vytváranie takýchto programov nazývame metaprogramovaním.

Metaprogramovanie teda definujeme ako vytváranie programov, ktoré manipulujú programami ako dátami — analyzujú, generujú alebo transformujú iné programy alebo seba.

Už ste sa stretli s mnohými príkladmi takýchto programov. Sú to, samozrejme, všetky implementácie programovacích jazykov — prekladače a interpretátory, ktoré analyzujú programy a buď ich vykonávajú, alebo prekladajú do inej podoby, napríklad strojového kódu. Tak isto sem patria rôzne nástroje, ktoré používame pri programovaní a ktoré musia analyzovať programy a ich beh — debuggery, lintery, vývojové prostredia. Týka sa to aj nástrojov pre doménovo-špecifické jazyky. Spracovanie počítačových jazykov je však samostatná veľká oblasť, preto sa do metaprogramovania zväčša nezaraďuje.

Nástroje určené na syntaktickú analýzu ako sú Lex, Yacc, alebo Antlr tiež môžeme považovať za príklad metaprogramovania. Nielenže sa často používajú na analýzu kódu programov, ale aj samy generujú kód v nejakom programovacom jazyku. Iné generátory kódu tiež môžeme zaradiť k príkladom metaprogramovania.

Program však nemusí manipulovať inými programami, ale aj sam sebou. Príkladom toho sú makrá — fragmenty kódu, ktoré počas prekladu programu menia časť jeho kódu. Ďalším príkladom je reflexia, ktorá umožňuje programu analyzovať svoju štruktúru a stav počas behu.

Jednou z typických oblastí, kde sa často používa metaprogramovanie sú softvérové rámce, napríklad Spring, Hibernate, Ruby on Rails, alebo Django. Ich cieľom je poskytnúť všeobecné riešenie pre množstvo rôznych programov. Preto často používajú techniky metaprogramovania pre analyzovanie konkrétneho programu a prispôsobenie funkcionality pre jeho štruktúru.

Poznámka

Prípona „meta“ pochádza z gréčtiny (μετά), kde má význam „za” niečim, ale tiež „medzi“, „okrem“ a „mimo“. Používa sa napríklad v slovách metamorfóza (premena, zmena tvaru), metanoia (zmena zmýšľania, obrátenie, pokánie).

Zaujímavá je história vzniku pojmu metafyzika. Jeho autorom je filozof Andronikos z Rodu (1 storočie pred n.l.). Andronikos bol aj jedným z prvých vydavateľov diel Aristotela a jeho knihy o prvotných nehmotných príčinách bytia boli v zbierke zaradené za knihy o hmotnom svete, nazývané „fyzika“ (φυσικός — prirodzený) — takže boli nazvané „to, čo nasleduje za fyzikou“ (τὰ μετὰ τὰ φυσικά). V inom význame ide o to, čo vychádza za rámec hmotnej prírody, čo ho presahuje.

Tento význam metafyziky ako vedy vyššej úrovne, ktorá opisuje základné princípy, na ktorých je založená fyzika, ovplyvnil aj ďalšie použivanie prípony „meta“. Dnes sa často používa vo význame „o sebe“ (napríklad „metadáta“ sú dáta o dátach), alebo opísania niečoho na vyššej úrovni (napríklad „metateória“ je teória, ktorá skúma štruktúry a metódy nejakej inej teórie).

Reprezentácie programov

Program, manipulácia ktorým je podstatou metaprogramovania, môže mať rôzne podoby. Od vzniku po spustenie môže prechádzať týmito formami:

  1. Zdrojový kód — text v niektorom programovacom jazyku.
  2. Abstraktný syntaktický strom alebo graf (AST alebo ASG) — vnútorná reprezentácia kódu v prekladači alebo inom nástroji, ktorý ho analyzuje.
  3. Strojový kód alebo bajtkód — forma určená na vykonávanie buď priamo procesorom, alebo virtuálnym strojom.
  4. Proces — spustený program s údajmi a stavom v operačnej pamäti.

Tento zoznam, samozrejme, nie je úplný. Sú možné rôzne modifikácie alebo prechodné formy, napríklad medzikód používaný v procese prekladu a optimalizácie. V niektorých prípadoch, naopak, niektoré formy nemusia byť použité. Napríklad, interpretátory nemusia využívať strojový kód ani bajtkód.

Typy metaprogramovania

Reprezentácie programov súvisia aj s časom, kedy s programom pracujeme. Väčšinou sa rozlišujeme:

  • compile time — čas prekladu,
  • link time — čas zostavenia spustiteľného súboru,
  • load time — čas načítavania programu pred jeho spústaním,
  • run time — čas behu programu.

Zatiaľ čo v čase prekladu môžeme pracovať so zdrojovým kódom alebo jeho stromovou reprezentáciou, v čase načítavania už môžeme mať k dispozícii iba strojový kód alebo bajtkód (ak ide o kompilovaný jazyk). Proces je, prirodzene, k dispozícii iba počas behu.

Každému z týchto časov zodpovedajú špecifické techniky metaprogramovania. Napríklad makrá sú spracované v čase prekladu (compile time), zatiaľ čo reflexia funguje počas behu (run time).

Okrem toho môžeme rozlišovať objekt metaprogramovania, teda to, čim manipulujeme. Metaprogram môže manipulovať

  • iným programom,
  • sám sebou alebo svojou časťou.

Dôvody použitia metaprogramovania

Modularita

Napriek tomu sa metaprogramovanie úspešne používa v mnohých rámcoch, kde pomocou neho vieme dosiahnuť jednoduchší a ľahšie čitateľný kód a lepšie rozdelenie programu na moduly. Napríklad pri použití Hibernate naše triedy už nemusia riešiť aj ukladanie do databázy — tento „záujem“ (concern) bol oddelený od ich definície a prevzal ho rámec.

Problém modularity je však jedným z najpodstatnejších a najzložitejších problémov v informatike. Na jednej stráne modularita musí zabezpečiť rozdelenie záujmov (separation of concerns) — každý modul rieši len jednu konkrétnu úlohu a ostatné úlohy (záujmy) sú oddelené do iných modulov. Na druhej strane na to, aby moduly boli dostatočne oddelené, musia zabezpečiť skrývanie informácií (information hiding) od ostatných modulov, aby ich zmena nemusela vyžadovať aj zmenu iných modulov.

Súčasné splnenie týchto požiadaviek môže byť veľmi náročné, keďže môžu byť protichodné. Napríklad, majme triedu Product, ktorá reprezentuje informácie o produkte v obchode a jeho cenu. Ukladanie týchto informácií do databázy je samostatnou funkcionalitou, ktorú by sme chceli oddeliť do samostatného modulu, ktorý však musí poznať detaily reprezentácie produktu, a vedieť uložiť všetky jeho vlastnosti. Takže sa zdá, že oddelenie záujmov tu vyžaduje porušenie princípu skrývania informácií. Rámec Hibernate však využíva techniky metaprogramovania (reflexiu) na to, aby poskytol všeobecný modul ukladania do databázy, ktorý nie je závislý na konkrétnej implementácií triedy Product. Je to možné vďaka tomu, že dokáže automaticky zistiť jej štruktúru a prispôsobiť sa jej.

Metalingvistická abstrakcia

Podstatnou možnosťou využitia metaprogramovania je využitie metalingvistickej abstrakcie. Ide o vytváranie alebo modifikovanie jazyka, v ktorom bude vyjadrené implementované riešenie. Takže namiesto toho, aby sme prispôsobovali definíciu riešenia jazyku, ktorý máme k dispozíciu, vytvoríme nový jazyk, v ktorom dokážeme riešenie vyjadriť jednoduchšie.

V prípade, že takýmto spôsobom vytvárame nový jazyk, ide zväčša o doménovo-špecifický jazyk, teda jazyk zameraný na niektorú konkrétnu oblasť. Namiesto toho však môžeme modifikovať jazyk všeobecného použitia — doplniť do neho jazykové konštrukcie, potrebné pre jasné a prehľadné vyjadrenie nášho riešenia.

Riziká

Metaprogramovanie je pokročilou oblasťou programovania, ktorú programátori nepoužívajú bežne. Zároveň je to oblasť, ktorá umožňuje obchádzať bežné pravidla fungovania programov a programovacích jazykov a robiť to, čo sa za bežných okolností spraviť nedá, alebo len veľmi prácne a zložito. Použitie metaprogramovania môže pre neskúseného programátora vyzerať ako mágia.

„Any sufficiently advanced technology is indistinguishable from magic.“ — Arthur C. Clarke

Magické vlastnosti metaprogramovania sú jeho podstatnou nevýhodou. Program môže začať robiť niečo iné ako to, čo je priamo uvedené v jeho kóde, lebo nejaký iný program alebo časť samotného programu modifikuje jeho správanie. Alebo sú automaticky (niekedy sa hovori „automagicky“) generované časti programu, ktoré ste nepísali vy a teda im ani nerozumiete. Napríklad pomocou rámca Hibernate automaticky získate možnosť ukladať údaje vašich objektov do databázy bez toho, aby ste túto funkcionalitu implementovali.

Takéto zásahy do fungovania programu môžu viesť k neočakávaným výsledkom a k tomu, že programátori prestanú rozumieť kódu a nebudú v ňom môcť opravovať chyby a rozširovať ho. Zmena na jednom mieste môže spôsobiť neočakávané následky úplne inde.

Zdroje