TeX Alchemist Online

TeX のこと,フォントのこと,Mac のこと

TeX言語で foreach する (1)

多くのプログラミング言語で,(疑似コードでいうところの)

for i in 1...10 do {
  print(i)
}

のような foreach 文があります。これを同様の繰り返しを LaTeX で実現するにはどうすればよいでしょうか。

pgffor パッケージの \foreach

TeX Live に標準で含まれる pgffor パッケージをロードすることで,自由自在な \foreach が使えるようになります。

  1. 1,...,5 のように書けば公差1の等差数列
  2. 1,3,...,11 のように最初の二項を明示すれば任意の公差の等差数列
  3. 1,3,10,19 のように書けば任意に与えられた具体的な数列
  4. 2.と3.の記法を連結したもの

に関して,繰り返し処理を実行できます。

サンプル

\documentclass{article}
\usepackage{pgffor}
\begin{document}
% 公差1の等差数列
\foreach \i in {1,...,5} {[\i]} % => [1][2][3][4][5]
% 公差3の等差数列
\foreach \i in {4,7,...,19} {[\i]} % => [4][7][10][13][16][19]
% 公差-2の等差数列
\foreach \i in {7,5,...,1} {[\i]} % => [7][5][3][1]
% 任意に与えられる数列
\foreach \i in {4,9,1,3,10,11} {[\i]} % => [4][9][1][3][10][11]
% 任意に与えられる数列と,部分的に等差数列の合併
\foreach \i in {4,8,3,5,...,11,20,17,...,8,0} {[\i]} % => [4][8][3][5][7][9][11][20][17][14][11][8][0]
\end{document}

pgffor パッケージの不満点(というほどではないけれども)

この pgffor パッケージの \foreach がとても強力なので,これで十分事足りるのですが,強いて不満点を挙げるとするならば,次のような点でしょうか。

  • ,..., という部分のタイプ数が多い。
  • \foreach の内容部がグルーピングされる。

2点目は,たとえば次のようなことを指しています。

【入力】

\@tempcnta=0
\foreach \i in {1,...,5} {%
  current value = \the\@tempcnta\par
  \advance\@tempcnta\i
}%
final value = \the\@tempcnta\par

【出力】

current value = 0
current value = 0
current value = 0
current value = 0
current value = 0
final value = 0

普通のプログラミング言語の for ループの感覚で使っていると,この結果に当惑してしまうことでしょう。

これは,\foreach の内容部がグルーピングされているため,局所的な変数操作の結果はループごとにリセットされてしまうからです。 もちろん,\advance のところに \global を付けておけば大域的に変数の値を操作でき,結果を次のループに持ち越せるのですが,そうすると「スコープの一番外側」まで変数操作が波及してしまうことになり,思わぬ悪影響を及ぼす危険性があります。

自分で foreach を作ってみる

そこで,TeX言語の練習として,同じような foreach 文を,異なる書式で作ってみましょう。

サンプルソース

\documentclass{article}
\makeatletter

\long\def\FOREACH#1#2{%
  \def\FOREACH@BODY##1{#2}%
  \@FOREACH{#1}%
}
\def\@FOREACH#1{\@@FOREACH#1,\@empty\@nil}
\def\@@FOREACH#1,#2#3\@nil{%
  \@@@FOREACH#1-#1-\@nil
  \unless\ifx#2\@empty
    \@FOREACH{#2#3}%
  \fi
}
\def\@@@FOREACH#1-#2-#3\@nil{\@@@@FOREACH{#1}{#2}}

\newcount\FOREACH@start
\newcount\FOREACH@goal
\def\@@@@FOREACH#1#2{%
  \FOREACH@start=#1
  \FOREACH@goal=#2
  \ifnum\FOREACH@start<\FOREACH@goal
    \loop
      \FOREACH@BODY{\the\FOREACH@start}%
      \advance\FOREACH@start\@ne
    \unless\ifnum\FOREACH@start>\FOREACH@goal\repeat
  \else
    \loop
      \FOREACH@BODY{\the\FOREACH@start}%
      \advance\FOREACH@start\m@ne
    \unless\ifnum\FOREACH@start<\FOREACH@goal\repeat
  \fi
}
\makeatother

\begin{document}
% 単独
\FOREACH{1}{[#1]} % => [1]
% 増加列
\FOREACH{1-5}{[#1]} % => [1][2][3][4][5]
% 減少列
\FOREACH{5-1}{[#1]} % => [5][4][3][2][1]
% 任意の数列
\FOREACH{1,5,10,11}{[#1]} % => [1][5][10][11]
% これらの合併
\FOREACH{1,5-9,20-17,2-2}{[#1]} % => [1][5][6][7][8][9][20][19][18][17][2]
\end{document}

仕様

上記サンプルのように,

\FOREACH{添字集合}{繰り返し内容}

の書式で使い,{繰り返し内容} の部分ではループカウンタ変数を #1 で参照できます。

pgffor\foreach とは異なり,対応できる公差は ±1 だけですが,その場合に関しては添字集合が ,..., より簡潔に - で書けます。 (負の数を使いたいときは,{-1} のようにブレースで囲んでおけば大丈夫です。)

また,{繰り返し内容} が強制グルーピングされないので,次のような結果になります。

【入力】

\@tempcnta=0
\FOREACH{1-5}{%
  current value = \the\@tempcnta\par
  \advance\@tempcnta#1
}%
final value = \the\@tempcnta

【出力】

current value = 0
current value = 1
current value = 3
current value = 6
current value = 10
final value = 15

これで普通のプログラミング言語の for ループと同じ感覚で使えますね!

マクロの展開を追いかける

ここで定義した \FOREACH は,次のようにマクロを展開することで添字集合部分のパースを実現しています。

単独の場合

\@FOREACH{10}\@@FOREACH10,\@empty\@nil\@@@FOREACH10-10-\@nil\@@@@FOREACH{10}{10}

連続の場合

\@FOREACH{1-5}\@@FOREACH1-5,\@empty\@nil\@@@FOREACH1-5-1-5-\@nil\@@@@FOREACH{1}{5}

離散の場合

\@FOREACH{1,9}\@@FOREACH1,9,\@empty\@nil\@@@FOREACH1-1-\@nil + \@FOREACH{9,\@empty}

前者は

\@@@FOREACH1-1-\@nil\@@@@FOREACH{1}{1}

と展開され,後者は

\@FOREACH{9,\@empty}
\@@FOREACH9,\@empty,\@empty\@nil\@@@FOREACH9-9-\@nil\@@@@FOREACH{9}{9}

と展開されます。

混合の場合

\@FOREACH{1-5,8,10-12}\@@FOREACH1-5,8,10-12,\@empty\@nil\@@@FOREACH1-5-1-5-\@nil + \@FOREACH{8,10-12,\@empty}

前者は

\@@@FOREACH1-5-1-5-\@nil\@@@@FOREACH{1}{5}

と展開され,後者は

\@FOREACH{8,10-12,\@empty}\@@FOREACH8,10-12,\@empty,\@empty\@nil\@@@FOREACH8-8-\@nil + \@FOREACH{10-12,\@empty}

と展開されます。以下,これを再帰的に繰り返します。

以上を繰り返して,最終的に添字集合部分のパースが済むと,いずれも

\@@@@FOREACH{開始}{終了}

の形に展開されるので,後はこれを実装すればよいわけです。繰り返しの内容部は,はじめに \FOREACH@BODY という命令に収めて退避してありますので,これを呼び出せばOKです。

欠点

この実装の場合,ループの実装に \loop ~ \repeat を使っているので,ネスト(多重ループ)ができません。{\loop ~ \repeat} とブレースでくくればネスト可能な実装にも変更できますが,すると「中身をグルーピングしない」という長所が失われてしまうので,そこは一長一短と言えるでしょう。

この欠点の解決は次の記事で。

doratex.hatenablog.jp