Metaprogramování

Metaprogramování je programovací technika, založená na vytváření programů, které nakládají s jinými počítačovými programy jako se svými daty. Mohou je tedy modifikovat nebo vytvářet, případně i modifikovat samy sebe. To mimo jiné může umožnit již při kompilaci provádět činnosti, které by jinak musely proběhnout až za běhu programu.

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.

Zdroj