1

Ciekawe pętle i iteracje

Paweł Jackowski
TeX jest jednym z tych języków programowania, w których nie istnieje wbudowana pętla. Skoro jednak TeX doskonale znowi definicje rekursywne i potrafii sprawdzać warunki, pętle można sobie definiować samemu, w zależności od potrzeb.

Konstrukcje \loop...\repeat

Tradycyjna pętla

Oto tradycyjna postać pętli, znana z formatu plain:

\def\loop#1\repeat{\def\body{#1}\iterate}

\def\iterate{%
    \body\let\next\iterate
    \else\let\next\relax\fi
    \next}

\let\repeat\fi

Tę definicję warto pamiętać jako przejrzysty przykład powszechnie stosowanej w TeX-u rekusrji ogonowej (ang. tail recursion). Warto także zwrócić uwagę na znaczenie instrukcji \repeat (ostatnia linia kodu). Bez tego fgragmentu, nie można by użyć konstrukcji \loop...\repeat w pomijanej części warunku \if...\else...\fi.

Pętla z \expandafter

Zamiast niepotrzebnego przypisania \let\next\iterate wykonywanego przy każdej iteracji, użyć można polecenia \expandafter, które powoduje zniknięcie bloku warunku (\fi) przed wykonaniem iteracji. Pozbywamy się także niepotrzebnej definicji \next. Warto także zwrócić uwagę na to, że w przeciwieństwie do poprzedniej konstrukcji, możemy śmiało używać \else w warunku pętli.

\def\loop#1\repeat{\def\body{#1}\iterate}

\def\iterate{%
    \body\expandafter\iterate\fi}

\let\repeat\fi

Ponieważ \iterate i tak nie jest samodzielnym makrem, zamiast definiować \body, możemy każdorazowo zmieniać znaczenie \iterate.

\def\loop#1\repeat{%
    \def\iterate{#1\expandafter\iterate\fi}%
    \iterate}

\let\repeat\fi

Lukier syntaktyczny

Zamiast używać instrukcji \expandafter w każdej iteracji można użyć następującej konstrukcji.

\def\loop#1\repeat{%
    \def\iterate{#1\else\etareti\fi\iterate}%
    \iterate}

\def\etareti#1\iterate{\fi}

\let\repeat\fi

To podejście, podobnie jak postać tradycyjna, uniemożliwia stosowanie \else w warunku pętli. Można zatem zaproponować jeszcze inny sposób, w którym makro \iterate posiada parametr. Przy pierwszej iteracji parametr ten jest pusty, przy natępnych parametrem jest \fi.

\def\loop#1\repeat{%
    \def\iterate##1{##1#1\iterate\fi}%
    \iterate{}}

\let\repeat\fi

Pętla zagnieżdżalna

Oto propozycja pętli, którą można zagnieżdżać. Zamiast zapamiętywać zawartość pętli w jakiejś definicji (\body lub \iterate) jak w poprzednich pętlach, możemy przekazać tę zawartość jako parametr. Kosztem nieznacznego spowolnienia przetwarzania, zyskujemy możliwość zagnieżdżania.

\def\loop#1\repeat{\iterate{}\gobbleone{#1}}

\def\iterate#1\gobbleone#2{%
    #1#2\iterate\fi\gobbleone{#2}}

\def\gobbleone#1{}

\let\repeat\fi

Należy pamiętać, że aby użyć pętli wewnątrz innej pętli potrzebujemy znaków { oraz } determinujących sposób połykania przez TeX-a parametrów definicji.

\loop{
   ...
  \loop{
     ...
    \loop \repeat   
       ... 
    }\repeat  
  ...
  }\repeat

Należy podkreślić, iż użycie tutaj znaków początku i końca grupy nie oznacza, że pętle wewnętrzne wykonywane są w grupie. Grupowanie użyte jest wyłącznie po to, aby wskazać TeX-owi gdzie się kończą poszczególne pętle. Bez tych znaków TeX skończyłby czytać zawartość najbardziej zewnętrznej pętli zaraz po pierwszym napotkanym \repeat.

FIFO

Bez spacji

TeX-owe pętle to nie tylko konstrukcje \loop...\repeat. Możliwe jest także zastosowanie schematów przetwarzania FIFO (ang. first in first out). Często na przykład istnieje potrzeba wykonania jekiejś operacji dla nieznanej liczby kolejnych znaków, czy ogólniej — leksemów, pewnej grupy. Choć trudno zaprpoponować tu ogólne i uniwersalne konstrukcje, na podstawie poniższych przykładów użytkownik na pewno skonstruuje sobie własną przetwarzarkę FIFO.

\def\fifo#1{\ifx\ofif#1\ofif\fi\process#1\fifo}

\def\ofif#1\fifo{\fi}

\def\process#1{<cokolwiek>}

Przykładowo, użycie

\def\process#1{[#1]}

\fifo AB{CD}E\ofif

rozwinie się do [A][B][CD][E]. Można zaproponować odrobinę wolniejszą, choć wygodniejszą konstrukcję, w której nie jesteśmy skazani na używanie instrukcji \process. Mamy także swobodę w wyborze ogranicznika przetwarzanej listy znaków. Może nim być dowolny leksem, co do którego jesteśmy pewni, że nie pojawi się w przetwarzanym napisie. Dobrymi kandydatami są \relax, \null, czy \empty, choć zwykłe znaki też mogą spełniać tę rolę.

\long\def\foreach#1#2#3{%
    \ifx#2#3\expandafter\gobbletwo\else
    #1{#3}\expandafter\foreach\fi
    {#1}#2}
 
\def\gobbletwo#1#2{}

% test

\def\test#1{[#1]}
\foreach\test |AB{CD}E|

Ze spacją

Czytelnik na pewno zauważył, że powyższe przetwarzarki tak samo potraktują tekst ABCD jak i A B C D. Po pierwsze, już na bardzo wczesnym etapie przetwarzania, TeX redukuje liczę występujących po sobie białych znaków do jednego, po drugie, nawet ten jeden biały znak zniknie, jeśli wystąpi po dowolnej definicji.

Sprawa nie jest jednak beznadziejna. Możemy tymczasowo zmienić kategorię znaku spacji z 10 (blank space) na przykład na 12 (other). Wtedy spacja traktowana jest jak każdy inny znak — przekształcana jest na leksem znakowy i nie znika po instrukcjach zaczynających się od znaku \). Niestety traci wtedy także swoje wygodne w praktyce własności polegające na redukowaniu kilku białych znaków do jednego.

Jeśli przetwarzamy dane zdroworozsądkowych rozmiarów, zaproponować można inny sposób, nie wymagający zmiany kategorii znaków. Korzystamy tu z faktu, że spacja jako taka, tj. nie ujęta w nawiaty klamrowe, nie poprzedzona znakiem \ i ze swoją domyślną kategorią 10, może zostać pożarta jako parametr tylko w jednym przypadku; kiedy makro używa parametrów ograniczonych określonymi sekwencjami znaków. W tym przypadku ogranicznikiem jest sama spacja.

\def\endstr{\endstr}

\def\strdo#1#2{\strdostart{#1}#2 \endstr}

\def\strdostart#1#2 #3\endstr{%
    \foreach{#1}\endstr#2\endstr
    \ifx\endstr#3\endstr
      \expandafter\strstop
    \else
      #1{ }%
    \fi\strdostart{#1}#3\endstr}

\def\strstop#1\endstr{}

Zaczynamy od zdefiniowania makra \endstr, które oznacza koniec przetwarzanego napisu. Makro w tej postaci doskonale spełnia swoją rolę, jednak próba użycia go w innym kontekście może się skończyć katastrofą, gdyż jest to definicja redundantna. Tym co nie lubią brudnych trików można zaproponować \let\endstr\relax.

Pierwszym parametrem \strdo jest fragment kodu (instrukcja) który powtarzamy dla wszystkich znaków napisu. Drugim parametrem jest sam napis wzięty w nawiasy klamrowe.

\def\test#1{[#1]}

\strdo\test{abc}   % da [a][b][c]
\strdo\test{a b c} % da [a][ ][b][ ][c]
\strdo\test{ abc } % da [ ][a][b][c][ ]
\strdo\test{ }     % da [ ]
\strdo\test{}      % nic nie da

Jeśli chcemy jakąś operację wykonać nie dla wszystkich znaków, a dla wszystkich słów (ciągów znaków oddzielonych spacjami), sprawa robi się nieco prostsza.

\def\strwdo#1#2{\strwdostart{#1}#2 \endstr}

\def\strwdostart#1#2 #3{%
    \ifx\endstr#2\endstr\else#1{#2}\fi
    \ifx#3\endstr
      \expandafter\strstop
    \else
      \expandafter\strwdostart
    \fi{#1}#3}

Tutaj nie ma potrzeby połykania całego napisu aż do \endstr przy każdej iteracji. Należy także zauważyć, iż tutaj spacje występujące na początku i końcu napisu zostaną zignorowane.

Na deserek makro porównujące dwa ciągi znaków (i tylko znaków).

\def\strequal#1{\number\strequalstart{}{}#1\relax}
\def\strequalstart#1#2#3{\if#3\relax\strequalstop\fi
    \strequalstart{\if#3#1}{#2\fi}}

\def\strequalstop\fi\strequalstart#1#2#3{\fi#1#3\relax'#213 }

Konstrukcje eTeX-owe

...

Bibliografia

  1. Donald E.Knuth, The TeXBook,
  2. Alois Kabelschacht, \expandafter vs. \let and \def in conditionals and a generalization of plain's \loop, TUGboat, Volume 8 (1987), No. 2, 184–185.
  3. Kees van der Laan, FIFO and LIFO sing the BLUes, Biuletyn GUST nr 4 (1992), 20–26.
  4. Marcin Woliński, O pewnych konstrukcjach warunkowych i iteracyjnych, Biuletyn GUST nr 7 (1996), 5–9.
  5. Peter Breitenlohner, The e-TeX Manual, Version 2, February 1998, 9.
  6. Victor Eijkhout, The bag of tricks, TUGboat, Volume 21 (2000), No. 1, 91.
  7. Paweł Jackowski, Ciekawe pętle i iteracje na drugą nóżkę, Biuletyn GUST nr ? (2005), ?-?