6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TeX言語で再帰(1)― \loop ...\repeatを考える

Last updated at Posted at 2020-01-12

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
}

さらに,

  1. \else必須
  2. \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文(もどき)はできるか.

  1. この定義自体もTeX by Topicの11.8.3項で例示されています.

  2. この展開のシミュレーションの中の\relax\iterateの中のものですが,これが実際に展開の中に現れるかは<B>の終端が何かで変わります.その終端の要素が\relaxを「区切り」として要求するようなものであれば\relaxはそこで吸収されてしまいますので,実際には現れません.

  3. \@gobble\long\def\@gobble#1{}と定義されています.gobbleは「呑み込む」という意味です.

  4. TeX言語ではマクロ定義も代入です.

  5. tex.countはLuaTeXのテーブル(Lua言語での(仮想)テーブル)で,[ ]にTeXのcountレジスタの番号を指定するか,このようにカウンタ名を与えることで操作できます.\newcounter{cnt}で定義されるLaTeX2eのカウンタcntは内部で\newcout\c@cntとしてTeXのカウンタ\c@cntが定義されているので,tex.count[c@cnt]として扱えます.\@tempcnta\newcount\@tempcntaと定義されているので,ここでのように扱うことができます.

  6. そのままだと条件を満たしたときでもループしません.

  7. 正直,ロジック的な部分は全部Lua側で書いてしまえば,煩雑な処理を含めて展開可能にできるんじゃないかって気がしますが,それはそれで負けという気もしないでもないわけです.

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?