\loop...\repeat
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
.
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
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
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
.
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|
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 }
\expandafter
vs. \let
and \def
in conditionals and a generalization of plain's \loop
, TUGboat, Volume 8 (1987), No. 2, 184–185.