Hlavní navigace

Makro procesor GNU m4 (5)

Michal Burda

V dnešním pokračování se budeme věnovat rekurzivnímu expandování maker a využití rekurentní expanze při konstrukcích cyklů.

Rekurze a cykly

V makro procesoru m4 není přímá podpora pro cykly, ale to vůbec nevadí. Procesor totiž umožňuje rekurzi, s jejíž pomocí si příkazy pro cykly můžeme naprogramovat sami. Hloubka rekurze přitom prý není softwarově limitována – jak hluboko se procesor do expanze maker zanoří, závisí jen na vašem hardwaru a operačním systému.

Jen pro úplnost, rekurze je pojem označující, že se makro dožaduje expanze sebe sama (třeba s jinými parametry). Jednoduše lze vytvořit nekonečný cyklus a donutit procesor uváznout v nekončícím generování textu:

   define(`navzdy', `ahoj navzdy')
=>
   navzdy
=> ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj
   ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj
   ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj
   ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj
   ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj ahoj
   ahoj aho...

Nemyslím si, že byste nalezli nějakou užitečnou aplikaci tohoto příkazu. Čtěte dále, pokusíme se vymyslet něco potřebnějšího.

V m4 existuje makro jménem shift, které můžete spolu s ostatními příkazy použít k iteraci po argumentech makra:

shift(...)

Příkaz shift akceptuje jakýkoliv počet parametrů a expanduje na uvozený, čárkami oddělený seznam argumentů počínaje druhým:

   shift(ahoj)
=>
   shift(ahoj, čau, nazdar)
=> čau,nazdar

Následující příklad vypůjčený z manuálových stránek, ačkoliv není moc užitečný, nádherně ukazuje použití makra shift ve spojení s rekurzí:

   define(`obratit', `ifelse($#, 0, ,
                             $#, 1, ``$1'',
                             `obratit(shift($@)), `$1'')')
=>
   obratit
=>
   obratit(ahoj)
=> ahoj
   obratit(ahoj, čau, nazdar, hi)
=> hi, nazdar, čau, ahoj

Funkci tohoto makra si podrobně popíšeme. Skládá se z jednoho velkého příkazu ifelse, kterým se kontrolují okrajové podmínky:

  • makru nedodáme ani jeden argument ($#, 0, ) – potom obratit expanduje na prázdný řetězec;
  • makro voláme s jedním argumentem ($#, 1, ``$1'') – výsledkem bude uvozená hodnota tohoto parametru.

Pokud ani jedna z podmínek nenastane (tj. makro bylo voláno s více než jedním argumentem), dochází k rekurzivnímu volání sebe sama – ovšem s mírně upraveným seznamem argumentů, ve kterém je první parametr vypuštěn (shift($@)). Nově volané makro obratit tedy pracuje se seznamem argumentů o jeden parametr kratším. Po návratu z rekurze dojde k vypsání oddělovací čárky („,“) a prvního (předtím vypuštěného) argumentu („`$1'“). Pokud si příklad podrobně promyslíte, pochopíte, jak to hezky funguje.

Na rekurzivně volaném makru je nejdůležitější to, aby vůbec někdy skončilo. V drtivé většině případů proto bude obsahovat nějaký příkaz ifelse.

Nyní se můžeme pokusit vytvořit podmínkový příkaz podobný ifelse. Nazveme ho switch, protože je svou funkcí velice podobný stejnojmenému příkazu jazyka C. Jeho rozdíl oproti ifelse bude spočívat v tom, že se bude porovnávat vždy první a druhý, první a čtvrtý, první a šestý atd. argument, a pokud nastane shoda, expanduje makro na hodnotu třetího, pátého (popř. sedmého atd.) argumentu. Makro bude sloužit k porovnání nějaké hodnoty s řadou dalších hodnot, aniž bychom tu první museli neustále opakovat. Stejně jako u ifelse, i switch bude poslední argument chápat jako návratovou hodnotu („else“ část), pokud všechna porovnání selžou:

   define(`switch',
          `ifelse($#, 0, ,
                  $#, 1, ,
                  $#, 2, ,
                  `indir(`$switch', $@)')')
=>
   define(`$switch',
          `ifelse($#, 1, ,
                  $#, 2, ``$2'',
                  `$1', `$2', ``$3'',
                  `indir(`$switch', `$1', shift(shift(shift($@))))')')
=>
   switch(`pepa', `teta', `1 = 2',
                  `běta', `1 = 4',
                  `pepa', `1 = 6',
                  `žádná rovnost')
=> 1 = 6
   switch(`řepa', `teta', `1 = 2',
                  `běta', `1 = 4',
                  `pepa', `1 = 6',
                  `žádná rovnost')
=> žádná rovnost

Nyní jsme museli navíc použít pomocné vnitřní makro $switch, které spouštíme nepřímo přes indir. Makro switch volané s žádným, jedním nebo dvěma argumenty expanduje na prázdný řetězec, jinak se řízení předává makru $switch, které už pracuje, jak bylo řečeno.

Možná, že přijdete na lepší způsob, jak dosáhnout stejného efektu mnohem elegantněji. Rád se nechám poučit… :-)

Jako druhou ukázku zkusíme naimplemenjtovat jednoduchý cyklus for. Chceme vytvořit makro, které bude přebírat čtyři parametry. Prvním argumentem bude název iterační proměnné (makra), následovat bude počáteční hodnota, koncová hodnota proměnné a tělo cyklu, které se expanduje v průběhu každé iterace.

Cyklus budeme moci použít třeba pro jednoduchý výpis posloupnosti čísel:

   cyklus_for(`i', 1, 10, `i ')
=> 1 2 3 4 5 6 7 8 9 10

Dokonce jej bude možno vnořit:

   cyklus_for(`i', 1, 3, `cyklus_for(`j', 1, 4, `i:j ')
   ')
=> 1:1 1:2 1:3 1:4
=> 2:1 2:2 2:3 2:4
=> 3:1 3:2 3:3 3:4
=>

Naším požadavkům zhruba odpovídá následující makro:

  define(`cyklus_for',
    `pushdef(`$1', `$2')indir(`$for', `$1', `$2', `$3', `$4')popdef(`$1')')
=>

  define(`$for', `$4`'ifelse($1, `$3', ,
    `define(`$1', incr($1))indir(`$for', `$1', `$2', `$3', `$4')')')
=>

V uvedených definicích může čtenáře zaskočit makro incr, o kterém ještě nebyla řeč. Je to vestavěný příkaz, který jako jediný parametr přijímá celé číslo a jehož výsledkem je číslo o jedna vyšší.

Samotné makro cyklus_for uchová případnou předchozí definici proměnného makra $1, zavolá pomocné makro $for a po jeho skončení obnoví původní hodnotu proměnného makra $1.

Celá těžká práce spočívá na pomocném makru $for. To nejprve expanduje hodnotu čtvrtého argumentu (tělo cyklu) a pak kontroluje, dosáhla-li proměnná konečné hodnoty. V kladném případě expanduje na prázdný řetězec a končí, jinak číselnou hodnotu proměnného makra zvyšuje o jednotku a rekurzivně volá sám sebe.

Všimněte si opatrného zacházení s uvozovkami. Pouze tři argumenty nejsou uvozené a každý případ má samozřejmě své opodstatnění. Poprvé se neuvozený parametr objeví hned na začátku makra $for – tehdy chceme, aby m4 jeho obsah podrobil procesu hledání a expanze maker. Druhý neuvozený argument je v makru ifelse – tam potřebujeme, aby se proměnné makro expandovalo na číselnou hodnotu, a konečně potřetí je to ze stejného důvodu v argumentu makra indir.

Ačkoliv tohle makro už jistě dokážete využít v nějakém reálném případě, není ještě dostatečně robustní pro všestranné použití. Chybí mu jakákoliv obsluha chyb a testování, aby počáteční hodnota nebyla větší než koncová nebo nebo aby první argument byl možným identifikátorem makra. Za domácí úkol se to můžete pokusit napravit.

Pokračování příště.

Našli jste v článku chybu?