TeX言語にはfor
文のような明示的な繰り返し命令は組み込まれていません.TeX言語の言葉を使うならば,for
文のようなプリミティブはないということです.しかし,条件分岐と再帰があるので問題ないです.その上パターンマッチまであるのでもうばっちりです.つまりTeX言語は関数型です(まだいうか).
注意:手動で再帰を追いかけるので目(と頭)がちかちかします.書いててそうなってますし.
\loop ... \repeat
を考える
plain TeXの\loop ...\repeat
とLaTeX2eのものでは\long
があるかないかだけの違いしかないのですが,いちいち\long
をつけるのが面倒なので\long
なしで扱うことにします.
使い方:
\loop
<繰り返すもの>(この中に繰返しの終了条件のチェックが必ず必要)
\repaet
例:
\@tempcnt\z@
\loop
\ifnum\@tempcnta<6
\the\@tempcnta
\advance\@tempcnta\@ne %%%\ifnumに対する\fiは不要
\repeat
=>
012345
\loop
は条件が真である間は処理を繰り返します.\loop
の定義はTeX by Topicの11.8.3項でいろいろと論じられていますが,TeXLive2019のLaTeX2eの定義からlong
を削除したものを例にします1.
定義:
\def\loop#1\repeat{%
\def\iterate{#1\relax\expandafter\iterate\fi}%
\iterate
\let\iterate\relax}
\let\repeat\fi %%これ使われてない気がする.
この定義では,引数のパターンマッチで\loop
と\repeat
の間のものがすべて#1
に収められます.ここでは\if
で条件分岐を表すことにして\loop <A>\if<B>\repeat
という形で表記することにして展開を追いかけます.
\loop <A>\if<B>\repeat
=>
\def\iterate{<A>\if<B>\relax\expandafter\iterate\fi}%
\iterate
\let\iterate\relax
=>
\iterate
\let\iterate\relax
=>
<A>\if<B>\relax\expandafter\iterate\fi
\let\iterate\relax
=(\ifが真であれば)=>
<A><B>\relax\iterate\let\iterate\relax
=>
<A><B>\relax<A>\if<B>\relax\expandafter\iterate
\fi\let\iterate\relax
=(\ifが真であれば)=>
<A><B>\relax<A><B>\relax\expandafter\iterate\fi\let\iterate\relax
=>
<A><B>\relax<A><B>\relax\iterate\let\iterate\relax
=>
<A><B>\relax<A><B>\relax<A>\if<B>\relax\expandafter\iterate\fi\let\iterate\relax
=(\ifが偽になれば)=>
<A><B>\relax<A><B>\relax<A>\let\iterate\relax %%\if ... \fiの間は展開されず読み飛ばされる
このように展開されます.まず,注目すべき点は\iterate
の出現位置です.最初の出現箇所は定義の直後で,これが展開されて<A>\if<B>\relax\expandafter\iterate\fi
が現れます.これによって\if ...\fi
の構文が完成します.さらに条件が真の場合は,\expandafter
で\iterate
が飛び越えられて\fi
に到達するので,<A><B>\relax\iterate
になります.再び\iterate
を展開すれば必要な分だけ繰り返しが現れます.ただし,<A>
か<B>
の中で条件が偽になるような処理が起こらなければ無限ループになります.
また条件が最初から偽であった場合は,最初の\iterate
の展開<A>\if<B>\relax\expandafter\iterate\fi
で\if ...\fi
が読み飛ばされるので<A>
だけが現れます.さらに.複数回実行される場合でも,最後の繰り返しでは<A>
が現れます.したがって,実際のところは
\loop<A>\if<B>\repeat
=>
<A> %%これは最低でも一回は現れる
<B>\relax<A>
<B>\relax<A>
...
<B>\relax<A>%%%実際に繰り返されたときの最後にも<A>は現れる
と展開されることになります2.しかし,この繰り返しはこのままではあまり直感的ではありません.そこで少しだけ細工してみます.
\def\initialize{\@tempcnta\z@\def\initialize{}}
\loop\initialize
\ifnum\@tempcnta<6
\the\@tempcnta
\advance\@tempcnta\@ne
\repeat
こうすると<A>
の部分は実質的に最初の一回だけ処理され,初期化のような形にできます.また<B>
の部分を空にしたり,繰り返しの制御変数(この例では\@tempcnta
)をどのタイミングでどのように変化させるか,条件分岐をどうするか(\if
ではなく\unless\if
にするとか)でも繰り返しを変化させることができます.柔軟性があるといえばそうですが,柔らかすぎてちょっと間違えると期待通りにならないという部分は否定できません.
\loop ... \repeat
を書き換えてみる
自明な調整
\def\loop#1\repeat{%
\def\iterate{#1\relax\expandafter\iterate\fi}%
\iterate
\let\iterate\relax}
まず,最後の\let\repeat\relax
です.最初に\iterate
を定義しているのでそれを無効にするということなのでしょうか.確かに\iterate
という名前は一般的で名前の衝突が起きそうですが,それならそれで\relax
にするのは余計に問題になりそうです.ということで,名前をもうちょっと衝突しなさそうな名前にします.\repeat
は繰り返しの対象がはっきりするので,このままにします.
\def\loop#1\repeat{%
\def\inside@loop{#1\relax\expandafter\inside@loop\fi}%
\inside@loop
}
ちょっとだけすっきりしますが,内部で\inside@loop
というマクロを定義しているのが何かいやなので,もうちょっと頑張ります.
\def
を頑張って排除したい
\inside@loop
を定義しているのは,繰返しの対象である#1
を何度も出現させることと,条件分岐で繰返しを終了させるためです.繰返しの様子だけ取り出してみると
\loop<A>\if<B>\repeat %%% #1を<A>\if<b>と表記
=>
<A>\if<B>\fi %% (1A) #1(<A>\if<B>)と\fiを(前の段階,最初だから何もないところに)追加
=(\ifは真とする)=>
<A><B>
=>
<A><B><A>\if<B>\fi %% (1A)の処理と同じ
=(\ifは真とする)=>
<A><B><A><B>
=>
<A><B><A><B><A>\if<B>\fi %% (1A)の処理と同じ
となっています.つまり繰返しのたびに<A>\if<B>\fi
,つまり#1\fi
を追加しています.末尾に\fi
を入れるというところに細工の余地があります.\fi
と<A>\if<B>
をマクロにして
\def\MACRO{\fi<A>\if<B>\fi}
としてみます.
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO %%% \def\loop#1\repeat{#1\MACRO}の気分
=>
<A>\if<B>\fi<A>\if<B>\fi %%\MACROの定義で先頭に\fiをいれたのはここで最初の\ifを消すため
=(\ifは真とする)=>
<A><B><A>\if<B>\fi
=(\ifは真とする)=>
<A><B><A><B>
当たり前ですが\if
が真なのに終わってしまいます.条件が真なのだから続いてほしいので\fi
への細工が連鎖するように
\def\MACRO{\fi<A>\if<B>\MACRO}
と変えてみます.
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO %%% \def\loop#1\repeat{#1\MACRO}の気分
=>
<A>\if<B>\fi<A>\if<B>\MACRO
=(\ifは真とする)=>
<A><B><A>\if<B>\MACRO
=>
<A><B><A>\if<B>\fi<A>\if<B>\MACRO
=(\ifは真とする)=>
<A><B><A><B><A>\if<B>\MACRO
=>
...
うまく連鎖します.このまま放置だといつまでも終わらないので終了条件を考えます.\if
が偽の場合に終了するように\MACRO
を飛び越えましょう.
\def\MACRO{\fi<A>\if<B>\MACRO} %%% 無限ループ版
\def\MACRO{\fi<A>\if<B>\MACRO\fi}
さて展開を行います.
\def\MACRO{\fi<A>\if<B>\MACRO\fi}
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO
=>
<A>\if<B>\fi<A>\if<B>\MACRO\fi
=(\ifは偽とする)=>
<A><A>\if<B>\MACRO\fi
=(\ifは偽とする)=>
<A><A>
繰り返さずに停止はしますが<A>
が二回でてきました.これではだめです.\MACRO
の中の<A>
が条件分岐の外にでてしまっているのが原因です.\MACRO
の中が\fi<A>\if<B>\fi
ではなく<A>\fi\if<B>\fi
ならいいのですが,<A>\if<B>
は繰返しの本体(#1
で与えられるもの)なので分解したくありません.そこで,\loop<A>\if<B>\repeat
の方を変更します.
\def\MACRO{\fi<A>\if<B>\MACRO\fi}
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO\fi %%%\fiを明示してみた(これが\loop ...\repeatの変更点)
=(\ifは偽とする)=>
<A>
停止します.手をいれたのでまた真の場合を考えます.
\def\MACRO{\fi<A>\if<B>\MACRO\fi}
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO\fi %%%\fiを明示してみた
=(\ifは真とする)=>
<A><B>\MACRO\fi %%分岐の真の場合の展開中
=>
<A><B>\fi<A>\if<B>\MACRO\fi\fi %%最初の\fiは条件分岐の終端となって消える
=>
<A><B><A>\if<B>\MACRO\fi\fi
だめです.\fi
が余計です.最後の\fi
が余計なのでこれをとりたいです.\MACRO
の展開の際に消えればよいので\MACRO
の引数のパターンマッチを使います.
\def\MACRO\fi{\fi<A>\if<B>\MACRO\fi}
\loop<A>\if<B>\repeat
=>
<A>\if<B>\MACRO\fi
=(\ifは真とする)=>
<A><B>\MACRO\fi
=>
<A><B>\fi<A>\if<B>\MACRO\fi %%\MACROが\fiを呑み込んで展開
=>
<A><B><A>\if<B>\MACRO\fi %%条件分岐の終端で\fiが消える
同じ形ができたので,条件が真である限り展開が繰り返されます.ここで条件が偽に変わったとします.
<A><B><A>\if<B>\MACRO\fi %%前段階を再掲
=(\ifは偽とする)=>
<A><B><A>
停止しました.\MACRO
は次の繰返しにつなげるものなので名前を変えて\loop@next
としましょう.ここでまとめてみます.<A>\if<B>
は\loop#1\repeat
の#1
なので#1
に戻します.
\def\loop#1\repeat{%
\def\loop@next\fi{\fi#1\loop@next\fi}%
#1\loop@next\fi
}
これでは複雑になっているうえに,そもそも\def
を外に出したいのに中に残ってます.外に出してしまいましょう.
\def\loop@next\fi{\fi#1\loop@next\fi}%
\def\loop#1\repeat{%
#1\loop@next\fi
}
もちろんこれでは#1
の扱いがなってません.\loop@next
に引数を与えましょう.
\def\loop@next#1\fi{\fi#1\loop@next#1\fi}%
こうすると\fi
のパターンマッチで,{#1}
としなくても引数として扱えます.\loop@next
の中の\loop@next
もきちんと同じ形式の引数が渡るようにしています.\loop ...\repeat
の中での\loop@next
の呼び出しも形をそろえます.
\def\loop@next#1\fi{\fi#1\loop@next#1\fi}%
\def\loop#1\repeat{%
#1\loop@next#1\fi
}
\loop<A>\if<B>repeat
=>
<A>\if<B>\loop@next<A>\if<B>\fi
考えるまでもなくダメでした.#1
の中に\if
がいるので,\if ...\fi
の対応がとれません.となると,#1
は\if ...\fi
の外におくしかありません.
\def\loop@next\fi#1{\fi#1\loop@next\fi{#1}}
\def\loop#1\repeat{%
#1\loop@next\fi{#1}% %%末尾に % が必要
}
\loop<A>\if<B>repeat
=>
<A>\if<B>\loop@next\fi{#1}%%後ろは#1のままにすると見やすい
=(偽)=>
<A><A>\if<B> %%#1を<A>\if<B>に書き直した
これも論外です.末尾につけた#1
(<A>\if<B>
)が残ってしまいます.最初から偽の場合には末尾の#1
が消えなければいけません.引数を無視したい場合の定石のマクロとしてはLaTeX2eの\@gobble
があります3.そこで単純に
\def\loop@next\fi#1{\fi#1\loop@next\fi{#1}}%
\def\loop#1\repeat{%
#1\loop@next\else\expandafter\@gobble\fi{#1}%
}
と書いてみますが,\loop@next
の定義では直後に\fi
が必要です.そこで\loop@next
と\fi
の前に何かがあってもいいように\loop@next
を修正します.
\def\loop@next#1\fi#2{\fi#2\loop@next\fi{#2}}%
これで引数のパターンの不一致は解消されます.引数の番号がずれていることに注意してください.また展開を追いかけます.
\def\loop@next#1\fi#2{\fi#2\loop@next\fi{#2}}%
\def\loop#1\repeat{%
#1\loop@next\else\expandafter\@gobble\fi{#1}%
}
\loop<A>\if<B>\repeat
=>
<A>\if<B>\loop@next\else\expandafter\@gobble\fi{#1}% 偽の場合は \if...\elseが展開なしで消える
=(偽)=>
<A>\expandafter\@gobble\fi{#1}%
=>
<A>\@gobble{#1}% %\expandafterによって\fiに先に到達
=>
<A> %%停止
\loop<A>\if<B>\repeat
=>
<A>\if<B>\loop@next\else\expandafter\@gobble\fi{#1}%
=(真)=>
<A><B>\loop@next\else\expandafter\@gobble\fi{#1}%
%\elseから\@gobbleまでは\loop@nextの#1で消えてしまう
=>
<A><B>\fi<A>\if<B>\loop@next\fi{#1}%
=>
<A><B><A>\if<B>\loop@next\fi{#1}% \loop@nextの第一引数は空になる
=(真)=>
<A><B><A><B>\fi<A>\if<B>\loop@next\fi{#1}%
=>
<A><B><A><B><A>\if<B>\loop@next\fi{#1}%
=(偽)=>
<A><B><A><B><A>{#1}%
繰返しのあとに条件が偽になれば停止しますが,また#1
に相当する部分が残ってしまいます.これはささきほどのケースと全く同じです.偽のときに\fi
を飛び越えて{#1}
を消すことができればよいのです.したがって,
\def\loop@next#1\fi#2{\fi#2\loop@next\else\expandafter\@gobbble\fi{#2}}
とすれば,繰返し後の停止で{#1}
が残ることはありません.また,\loop@next
の展開において\fi#2
以降は\loop ...\repeat
の最初の展開と同じなので上記の展開と同じことが連鎖することが分かります.したがって,
\def\loop@next#1\fi#2{\fi#2\loop@next\else\expandafter\@gobble\fi{#2}}
\def\loop#1\repeat{%
#1\loop@next\else\expandafter\@gobble\fi{#1}%
}
が得られます.内側の\loop...\repeat
から\def
を排除できました.もうちょっとこだわってみます.\expandafter\@gobble
が気になります.\fi
を処理して{#1}
を消すということで,
\def\loop@stop\fi#1{\fi}
というものを考えます.パターンマッチで\fi
をとり,{#1}
を引数として受け取ります.この引数を無視することで引数を呑み込み,\fi
を展開結果としているので条件分岐の終端となって,結果として同じことになります.さらに\loop
という名前の重複を避けて,今までのことを合わせて以下の定義が得られます.
\def\Loop@next#1\fi#2{\fi#2\Loop@next\else\Loop@stop\fi{#2}}
\def\Loop@stop\fi#1{\fi}
\def\Loop#1\repeat{%
#1\Loop@next\else\Loop@stop\fi{#1}%
}
補助マクロを二つ追加しましたが\def
を排除できました.いろいろやってますが,この場合は,結論のマクロそのものは慣れると案外あっさりできます.ただし,このような展開の追いかけはTeX言語では重要ですし,必要に応じて微妙にコードを調整していく工程も大切だと思います.見ての通り,同じコードが二か所にあるので,そこもまとめることができると思いますが,これくらいの方が可読性がよさそうなのでこれくらいで.
\def
の排除を頑張ったけど・・・
マクロの内部に\def
や代入が存在する4とそのマクロは展開だけの処理ではなくなります.こういう状態を「完全展開できない」と表現します.個人的には,\loop
のような再帰的なマクロは可能な限り展開だけで行いたい(再帰をつかさどる部分は完全展開可能にしたい)と思っています.またマクロの中でマクロを定義するのは,\newcommand
のような「マクロを定義するマクロ」以外は避けたいです.そこでこのような書き換えをしてみました.
しかし実際のところ,\loop
のような汎用的な繰り返しでは,処理の過程で何らかの状態が変化し,変化して保存された状態によって繰返しが継続されるか停止されるかが判断されます.しかもその状態変化は\loop
側ではなく,ユーザが指定するものです.これが\loop
の汎用性と使いにくさの原因ですが,本質的に何らかの形での「状態の保存」か少なくとも「状態の伝達」が必須であり,いくら頑張っても\loop ...\repeat
の枠組みそのものは「展開のみ」であっても全体としては「展開以外もありうる」というわけです.
したがって,\loop
を展開可能な形で定義するのはある意味では無駄です,しかし,「純粋に展開だけで動くマクロには特別な魅力がある(There is a particular charm to macros that work purely by expansion)」(TeX by Topic, 12.6.9)という主張もあります.実際,こういう展開の連鎖って面白くないですか?
LuaTeXを使ってみる
とはいいつつも手を考えます.困ったときはLuaTeX.状態の変化とその状態の保存が問題ならそこをLuaに任せることにします.
\def\Loop@next#1\fi#2{\fi#2\Loop@next\else\Loop@stop\fi{#2}}
\def\Loop@stop\fi#1{\fi}
\def\Loop#1\repeat{%
#1\Loop@next\else\Loop@stop\fi{#1}%
}
\@tempcnta=z@
\count@=\Loop
\ifnum\@tempcnta<4
\directlua{tex.count["@tempcnta"]=tex.count["@tempcnta"]+1}% %% Luaっ!! 行末の%は必要
\the\@tempcnta
\repeat\relax%%\relaxで終端を明示
\count@
の値はいくつになるでしょうか?
答えは「1234」になります.\count@=
で=
の後ろには数字が期待されるので,TeXの仕様「数字が期待されるところはどんどん展開」が働きます.そこで\Loop
が展開されますが,いま状態「\@tempcnta
」の加算と代入は\directlua
の中でLuaのコードとして書かれています5.そして\directlua
は展開可能なのです.\Loop
も展開可能になるように定義していますので全体として展開可能になりますので,\Loop
の結果である「1234」が途中に何もない(\relax
とか代入がない)数字になって\count@
に代入されます.
このようにLuaTeXを使えばなんとかなりますが,使わずに何とかならないだろうかという気は捨てきれません.実際もっといろいろな制限を加えてなんとかなるはずなのではと思いますが,ちょっと長くなりそうなのでここで打ち止めにしておきます.
\loop ...\if ...\else ...\repeat
への拡張(TeX by Topicより)
TeX by Topicの11.8.3には\loop ... \if ... \else ... \repeat
の形の\loop
があります.
その定義が以下ですが,誤植があるので修正しています6.またこの定義では\else
がないと条件が真のときに\let\next\relax
があるので停止してしまいますので\else
が必須です.
\def\loop#1\repeat{%
\def\body{#1}%
\iterate}
\def\iterate{%
\let\next\iterate %%TeX by Topicでは\let\next\relax
\body
\let\next\relax %%TeX by Topicでは\let\next\iterate
\fi
\next
}
これをLaTeX2e版の定義に合わせて書き直します.
\def\loop#1\repeat{%
\def\iterate{#1\let\iterate\relax\fi\iterate}
\iterate
}
さらに,
-
\else
必須 -
\loop ...\if ...\else ...\repeat
の枠組みは展開可能
となるように書き換えます.\else
がない場合も合わせてマクロ名も変えます.展開可能にする過程はほぼ同じなのですし類推も可能ですので省略します.
\def\Loop@withoutElse@next#1\fi#2{%
\fi#2\Loop@withoutElse@next\else\Loop@withoutElse@stop\fi{#2}}
\def\Loop@withoutElse@stop\fi#1{\fi}
\def\Loop@withoutElse#1\repeat{%
#1\Loop@withoutElse@next\else\Loop@withoutElse@stop\fi{#1}%
}
\def\Loop@withElse@next#1\fi#2\else#3\endarg{%
\fi#2\Loop@withElse@next\else#3\Loop@withElse@stop\fi#2\else#3\endarg}
\def\Loop@withElse@stop\fi#1\else#2\endarg{\fi}
\def\Loop@withElse#1\else#2\repeat{%
#1\Loop@withElse@next\else#2\Loop@withElse@stop\fi#1\else#2\endarg%
}
二つのloopを統合してみる
\else
なし版\Loop@withoutElse
,\else
あり版\Loop@withElse
ができましたが,いちいち使い分けるのは面倒です.\else
の有無を判別して処理をわけるようにしましょう.上述の二つのloop,\Loop@withoutElse
と\Loop@withElse
を前提とします.また,TeXLive2019で追加されたプリミティブ\expanded
を,\Loop#1\repeat
の#1
に\else
節が存在するかをチェックしてその結果を\ifx
に渡すために使っています.\else
が存在すればそこの最初の文字(トークン)を取得するようマクロをその場で展開しきるようにできるのです.それ以外はマクロの引数のパターンマッチの組み合わせです.
\def\firsttoken#1#2\endarg{#1}
\def\check@else#1\else#2\fi{\firsttoken#2\@empty\endarg}
\def\Loop#1\repeat{%
\expandafter\ifx\expanded{\check@else#1\else\fi}\@empty\@empty
\Loop@withoutElse#1\repeat
\else
\Loop@withElse#1\repeat
\fi
}
これはこれでこのマクロは動きますが,正直もうちょっとすっきりしたいです.\expanded
なしでうまいこといく方法はないかとか,繰返しの本体が条件分岐の中にあるのもなんかすっきりしません.けどうまい手が思いつかないので\loop ...\repear
はここまでにします.
おわりに
\loop ...\repeat
だけでここまで引っ張ってみました.結論は「LuaTeX強い」7じゃなくって,展開にこだわると複雑になるなぁということでしょうか.だがしかし,こだわります.次回に続け.
次回予告:展開可能なFOR文(もどき)はできるか.
-
この定義自体もTeX by Topicの11.8.3項で例示されています. ↩
-
この展開のシミュレーションの中の
\relax
は\iterate
の中のものですが,これが実際に展開の中に現れるかは<B>
の終端が何かで変わります.その終端の要素が\relax
を「区切り」として要求するようなものであれば\relax
はそこで吸収されてしまいますので,実際には現れません. ↩ -
\@gobble
は\long\def\@gobble#1{}
と定義されています.gobbleは「呑み込む」という意味です. ↩ -
TeX言語ではマクロ定義も代入です. ↩
-
tex.count
はLuaTeXのテーブル(Lua言語での(仮想)テーブル)で,[ ]
にTeXのcountレジスタの番号を指定するか,このようにカウンタ名を与えることで操作できます.\newcounter{cnt}
で定義されるLaTeX2eのカウンタcnt
は内部で\newcout\c@cnt
としてTeXのカウンタ\c@cnt
が定義されているので,tex.count[c@cnt]
として扱えます.\@tempcnta
は\newcount\@tempcnta
と定義されているので,ここでのように扱うことができます. ↩ -
そのままだと条件を満たしたときでもループしません. ↩
-
正直,ロジック的な部分は全部Lua側で書いてしまえば,煩雑な処理を含めて展開可能にできるんじゃないかって気がしますが,それはそれで負けという気もしないでもないわけです. ↩