Metaprogramování
Metaprogramování je skupinou programovacích technik, které umožňují psaní programů, které vytváří další programy nebo s nimi nakládají jako se svými daty. Případně se může jednat o programy, které při svém běhu mohou dělat činnosti, které by jinak musely provádět již při kompilaci.
V mnoha případech tato technika umožňuje programátorovi napsat program úsporněji (zmenšit počet jeho řádků, zjednodušit jeho vnitřní strukturu). Může také pomoci s odstraněním duplikovaného kódu v rámci programu. Jazyk, ve kterém je metaprogram napsán, se nazývá metajazykem. Jazyk programu, se kterým metaprogram manipuluje, se nazývá předmětným jazykem. Schopnost programovacího jazyka být svým vlastním metajazykem se často nazývá reflexí. Reflexe je schopnost programovacího jazyka, která programu v něm napsaném umožňuje zjistit informace o sobě samém.
Způsoby metaprogramování
Prvním způsob je odhalení vnitřní stavby programu za běhu pomocí API (aplikačního programovacího rozhraní). Program psaný v jazyce, který umožňuje objektově orientované programování, tak za běhu může například zjistit, jaké třídy je daný objekt, jaké metody tato třída definuje, jaké mají tyto metody parametry atd. Může také s těmito strukturami manipulovat - například za běhu programu vytvářet nové třídy, měnit implementace již definovaných metod, apod. Vždy záleží na konkrétním programovacím jazyku, jeho vlastnostech a dynamičnosti.
Druhým způsobem je dynamický překlad a spuštění zdrojového kódu z výrazu, který je dynamicky generován za běhu programu. Často se jedná o řetězce, které jsou za běhu programem dynamicky sestaveny a interpretovány jako zdrojový kód. Programátor může kromě podoby interpretovaného řetězce také ovlivnit kontext, ve kterém je výsledný zdrojový kód interpretován. Díky tomu tak doslova "programy mohou vytvářet programy".
Třetím způsobem je úplně vystoupit z předmětného jazyka a použít pouze metajazyk. Univerzální systémy pro transformaci programů, které jako svůj vstup přijímají popisy jazyků a poté mohou nad tímto jazykem provádět libovolné transformace, jsou přímou implementací metaprogramování. Tento přístup umožňuje použít techniky metaprogramování i ve spojení s jazyky, které nemají žádnou vlastní podporu pro metaprogramování.
Statické jazyky
Statické jazyky většinou neumožňují přímou interpretaci zdrojového kódu za běhu programu. Zdrojový kód musí být nejdříve zkompilován do strojového kódu, nebo bytekódu, než může být počítačem interpretován. Je však možné metaprogramování využít před tím, než je program zkompilován. Před finální kompilací programu rozhodneme, které jeho části využijeme (například pomocí podmínek v makrech) a budeme kompilovat a které části během kompilace "zahodíme". Příkladem takové techniky mohou být makra v C nebo šablony v C++.
Dynamické jazyky
Některé dynamické jazyky dovolují programátorovi manipulovat s jejich strukturou za běhu. Příkladem takových jazyků může být například Smalltalk, Lisp, Javascript, Ruby.
Příklady
Generativní programování
Následující skript v bashi je příkladem generativního programování:
#!/usr/bin/env bash
# metaprogram
echo '#!/usr/bin/env bash' >program
for ((I=1; I<=100; I++)) do
echo "echo $I" >>program
done
chmod +x program
Tento program (skript) vygeneruje nový program, který vytiskne na standardní výstup čísla 1 až 100. Slouží pouze pro ilustraci toho, jak jeden program může vytvořit jiný program. Výsledný program samozřejmě není nejefektivnější způsob, jak vypsat seznam čísel od 1 do 100.
Dynamické vytvoření třídy za běhu
classes = ["Foo", "Bar", "Baz"]
classes.each_with_index do |class_name, i|
klass = Class.new do
define_method :pozdrav do
puts "Já jsem #{self.class}, #{i+1}. dynamicky definovaná třída"
end
end
eval("#{class_name} = klass")
end
Baz.new.pozdrav # => Já jsem Baz, 3. dynamicky definovaná třída
Vysvětlení: Tento skript uloží do pole řetězce Foo, Bar a Baz. Poté toto pole projde a dynamicky definuje 3 třídy. Každá z těchto tříd má definovanou svou vlastní instanční metodu pozdrav
, která vypíše jméno třídy, jejíž je instancí, a svoje pořadové číslo, které je v definici funkce přístupné díky uzávěře, která vzniká díky využití bloku pro definování metody.
Poté je vytvořen řetězec, který je ihned interpretován pomocí metody eval
. Tento řetězec obsahuje jednoduché vytvoření nové konstanty, do které je přiřazena nově vytvořená třída.
V tomto příkladě se tedy jedná o kombinaci prvního a druhého výše zmíněného způsobu metaprogramování.
Dynamické přepsání metody
Příklad dynamického přidání funkcionality již definované metodě v programovacím jazyce Ruby:
class Foo
def print_bar
puts "bar"
end
end
foo = Foo.new
foo.print_bar # => bar
Foo.class_eval do
alias :print_bar_without_baz :print_bar
def print_bar_with_baz
print "baz "
print_bar_without_baz
end
alias :print_bar :print_bar_with_baz
end
foo.print_bar # => baz bar
Vysvětlení: Nejdříve definuje třídu Foo. V této třídě definujeme jednoduchou metodu, která vytiskne na STDOUT (standardní výstup) text "bar". Vytvoříme novou instanci třídy Foo a zavoláme na ní metodu print_bar
. Na výstupu bude po spuštění programu "bar"
Poté v kontextu třídy Foo
(Foo.class_eval
) spustíme blok kódu, ve kterém můžeme s již předtím definovanou třídou manipulovat. Vytvoříme alias (alternativní jméno) print_bar_without_baz
metody print_bar
. Můžeme tak stejnou metodu volat pod dvěma jmény print_bar i print_bar_without_baz. Vždy se stejným výsledkem.
Dále definujeme novou metodu print_with_baz
. Tato metoda nejdříve vypíše baz
a následně volá již dříve definovanou metodu print_bar
pod jejím novým alternativním jménem print_without_baz
.
Na dalším řádku vytvoříme alias print_bar
nově definované metody print_bar_with_baz
. Tento nový alias přepíše původně definovanou metodu "print_bar".
Znovu zavoláme metodu print_bar
na instanci třídy Foo
(uložené v proměnné foo
). Výsledkem je tentokrát text "baz bar".
Podařilo se nám tedy "předefinovat" již dříve vytvořenou metodu, kterou jsme uložili pod jiným jménem. Pokud po tomto předefinování metody voláme na instancích pozměněné třídy tuto metodu, dostaneme jiný výsledek. Tento přístup programátorům umožňuje jednoduše přidávat funkcionalitu do knihoven třetích stran, bez toho, aby museli upravovat zdrojový kód těchto knihoven. Pokud programátorovi nevyhovuje konkrétní chování některé metody z knihovny, může ji předefinovat a využívat svoji pozměněnou implementaci, avšak se zachováním možnosti volání původní metody knihovny, která je poté dostupná pod jiným jménem.
Reference
V tomto článku byl použit překlad textu z článku Metaprogramming na anglické Wikipedii.