Makrá

Makrá v C

Makrá sú jedným zo spôsobov generovania kódu. Makro definuje vzor, ktorý bude vyhľadávaný v kóde programu a pravidlo čim bude tento kód nahradený. Typickým prípadom sú makrá v jazyku C, ktoré sú definované pomocou #define.

Zoberme si ako príklad makro umožňujúce výmenu hodnôt dvoch premenných:

#define SWAP(a, b, type) type t = a; a = b; b = t

Makro môžeme použiť v kóde podobne ako voláme funkcie:

int x = 1, y = 2;
SWAP(x, y, int);
printf("x=%d, y=%d\n", x, y);

Na rozdiel od volania funkcie, ktoré sa uskutoční počas behu programu, makro sa vykoná už počas kompilácie. Výsledkom vykonania makra bude nahradenie jeho použitia pomocou tela makra:

int x = 1, y = 2;
int t = x; x = y; y = t;
printf("x=%d, y=%d\n", x, y);

To znamená, že napriek podobnosti použitia, makrá fungujú úplne inak ako funkcie. Jednou z výhod makra oproti funkcií môže byť väčšia efektivita, keďže počas vykonania odpadá potreba vytvárať nový aktivačný záznam na zásobníku volaní a prepínať riadenie do inej funkcie a naspäť. To sa však v súčastnosti dá riešiť aj pomocou inline funkcií.

Podstatnejšou výhodou je to, že makro môže obchádzať niektoré obmedzenia jazyka — jeho syntaxe a štandardného spôsobu vyhodnocovania. Napríklad makro SWAP má typ premennej ako parameter, čo by sa pri funkcii nedalo realizovať. Makrá tiež umožňujú implementovať niečo, čo vyzerá ako nové syntaktické konštrukcie. Napríklad rámec pre testovanie Googletest poskytuje takúto konštrukciu pre definíciu testu, kde za slovom TEST nasleduje názov sady testov, názov testu a telo testu:

TEST(FactorialTest, HandlesZeroInput) {
  EXPECT_EQ(Factorial(0), 1);
}

Poznámka

Iné jazyky môžu poskytovať bohatšie vyjadrovacie schopnosti, ktoré umožnia realizovať podobné syntaktické konštrukcie bez použitia makier. Napríklad toto je príklad definície testov v jazyku Kotlin pomocou knižnice Kotest:

class MyTests : StringSpec({
  "length should return size of string" {
    "hello".length shouldBe 5
  }
  "startsWith should test for a prefix" {
    "world" should startWith("wor")
  }
})

Problémy textových makier

Okrem užitočných vlastností majú makrá aj viacero problémov. Textové makrá, ktoré sa používajú v jazyku C, pracujú s kódom programu ako s jednoduchým textom a preto po ich nahradení sa môže stať, že výsledok nebude syntakticky alebo sémanticky správny.

Vráťme sa k príkladu makra SWAP. Ak ho použijeme vo vnútri iného príkazu, môže nastať problém.

int x = 1, y = 2;
if (x > y)
    SWAP(x, y, int);
printf("x=%d, y=%d\n", x, y);

Po nahradení makra totiž iba prvý jeho príkaz bude súčasťou podmienky a ostatné zostanú mimo nej (upravil som formátovanie pre lepšiu čitateľnosť):

int x = 1, y = 2;
if (x > y)
    int t = x;
x = y; y = t;
printf("x=%d, y=%d\n", x, y);

Ďalší problém nastane v prípade, že v kóde, ktorý používa makro sa už používa premenná s názvom t. Obidva problémy sa dajú vyriešiť tým, že telo makra sa zabalí do príkazu do-while takto:

#define SWAP(a, b, type) do { type t = (a); (a) = (b); (b) = t; } while (0)

Syntaktické makrá

Mnohé problémy textových makier riešia syntaktické makrá, ktoré pracujú na úrovni syntaktického stromu a nie textu. Syntaktické makrá nenahradzujú fragment textu iným fragmentom textu, ale fragment syntaktického stromu iným fragmentom syntaktického stromu. Takéto makrá sa používajú napríklad v dialektoch jazyka LISP.

Na ilustráciu použijeme jazyk Racket, ktorý vychádza z jazyka Scheme. Tak ako všetky dialekty LISPu, aj Racket má nezvyčajnú syntax používajúcu veľké množstvo zátvoriek. Neprajníci dokonca tvrdia, že názov LISP znamená „lost in stupid parentheses“. Ako príklad si pozrime definíciu funkcie na výpočet n-tého člena Fibonacciho postupnosti:

(define (fib n)
  (cond
    ((= n 0) 0)
    ((= n 1) 1)
    (else (+ (fib (- n 1))
             (fib (- n 2))))))

Volanie tejto funkcie by vyzeralo jednoducho tak, že v zátvorkách uvedieme názov funkcie a jej argument (fib 5). Celý kód programu je vlastne zapísaný pomocou vnorených zoznamov uzavretých v zátvorkách. Týka sa to nielen volaní funkcií, ale aj operátorov a zabudovaných konštrukcií jazyka. Prvý prvok zoznamu označuje operáciu, ktorá sa má vykonať a zvyšok — jej argumenty. Tým pádom je kód v LISPe vlastne len serializovaný syntaktický strom s minimom doplňujúcej syntaxe.

Definícia makra pre výmenu premenných v Racket vyzerá následovne:

(define-syntax-rule (swap x y)
  (let ([tmp x])
    (set! x y)
    (set! y tmp)))

Definuje sa vzor a to, ako bude tento vzor nahradený, podobne ako je to v C. Rozdiel je však v tom, že tu sa vzor aj jeho náhrada spracovávajú ako zoznamy — fragmenty syntaktického stromu, čim odpadá značná časť problémov syntaktických makier, čo je opísané aj v dokumentácii makier založených na vzoroch.

Makrá môžu byť implementované nielen ako jednoduché náhrady, ale aj všeobecnejšie — ako funkcie manipulujúce fragmentom syntaktického stromu počas kompilácie. Dokumentácia Racket uvádza príklad makra swap ako transformačnej funkcie, ktorá najprv kontroluje, či zadané argumenty sú identifikátormi:

(define-syntax (swap stx)
  (syntax-case stx ()
    [(swap x y)
     (if (and (identifier? #'x)
              (identifier? #'y))
         #'(let ([tmp x])
             (set! x y)
             (set! y tmp))
         (raise-syntax-error #f
                             "not an identifier"
                             stx
                             (if (identifier? #'x)
                                 #'y
                                 #'x)))]))

Príklad definície makier a vysvetlenie významu ich použitia nájdete aj v článku Educational Pearl: Automata via Macros.