マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

\romannumeral の基本的な使い方(“関数”の合成)

\romannumeral\kansuji の両プリミティブの用法について、自信のない人は手許の TeX の参考書を参照して確認してほしい(おっと、\kansuji は「pTeX の参考書」が必要か)。しかし、文献によっては、最も基本的な使い方である「\romannumeral トリック」と「\kansuji トリック」についての記述が不十分な可能性もあるだろう。そこで、このブログで解説することにした。まずは、\romannumeral を紹介する。

\romannumeral の基本的な使い方

\romannumeral プリミティブは以下のような書式で用いられる。

\romannumeral-`0<トークン列S>

そして、このトークン列を一回展開した結果が、<トークン列S> の“先頭完全展開”、すなわち「先頭が展開不能トークンになるまで展開を反復した結果のトークン列」になる。(後で述べるように、これには例外が一つある。)

例えば、以下のように制御綴が定義されていたとする。

\def\A{\B\B}\def\B{\C\C}\def\C{\D\D}
\let\D\relax % \D は展開不能

この時、「\A\A」というトークン列について、展開を繰り返すと、

\A \A
→ \B \B \A
→ \C \C \B \A
→ \D \D \C \B \A

となり、この時点で列の先頭に展開不能トーク\D が現れる。従って、この「\D\D\C\B\A」が「\A\A」を“先頭完全展開”した結果ということになる。そして、\romannumeral を利用すると、“先頭完全展開”がただ一回の展開に転化することができる。

\romannumeral-`0\A\A
→ \D\D\C\B\A
\romannumeral を使って「関数を合成」する

このように \romannumeral を使うと展開を“加速”することができる。この性質は「完全展開可能なマクロの実装」において威力を発揮する。「完全展開可能なマクロ」はトークン列の間の“関数”と見なすことができるが、\romannumeral を使うと、そのような“関数”を“合成”することができるのである。

例えば以下のようなマクロ \chop を考える。*1これは引数の文字列(空白を含まない)について、末尾の文字を取り除いた文字列を返す。

%% \chop{<トークン列X>}
% 引数Xは"文字列"(カテゴリコード11または12の文字トークンから
% なるトークン列)とする。Xの末尾のトークンを除いたトークン列
% に展開される。Xが空列の場合は空列になる。先頭完全展開可能。
\def\chop#1{\xx@ifmt{#1}{}%
  {\xx@chop@a\xx@mk#1\xx@nil}}
\def\xx@chop@a#1\xx@mk#2#3\xx@nil{%
  \xx@ifmt{#3}{#1}{\xx@chop@a#1#2\xx@mk#3\xx@nil}}
\def\xx@mrk{\xx@mrk@}\def\xx@nil{\xx@nil@}
\def\xx@ifmt#1{\ifx\xx@mrk#1\xx@mrk\expandafter\@firstoftwo
  \else\expandafter\@secondoftwo\fi}

(先頭)完全展開可能であるので、次のように \typeout の中で使うことができる。

\typeout{[\chop{hello}][\chop{}]}%==>「[hell][]」と表示

「1 文字切り落とす“関数”」が得られているので、これを自分自身と“合成”すれば「2 文字切り落とす“関数”」が作れそうである。そこで問題であるが、実際に \chop を利用して「2 文字切り落とす」ための完全展開可能なマクロ \choptwo を実装してほしい。ただし、\chop の実装については、先に示した実装コードに記された「仕様」以上のことを仮定しないものとする。どうすればよいであろうか。

もちろん、次のような“最も素朴な実装”ではダメである。

\def\choptwo#1{%
  \chop{\chop{#1}}}

\chop の引数は、仕様上、“文字列”(文字トークンの列)でなければならない。ところが、\choptwo{filll} のように呼び出した場合、外側の \chop の引数になるのは「\chop{filll}」であり条件を満たしていない。希望としては、引数になるのはこのトークン列を完全展開した結果の「fill」であってほしい。・・・・・・となると、\expandafter をたくさん並べればよさそうである。

ところが、実際にはそれは上手くいかない。何故なら、「\chop{filll}」を何回展開すれば「fill」が得られるのかが事前に判らないからである。\expandafter 鎖を用いて引数を事前に展開させようとすると、「n 回展開するのに 2n−1 個の \expandafter が必要」なわけだから、事前に(十分な)展開回数を決めておいてそれに応じた個数の \expandafter を配置する必要がある。ところが、\chop の仕様では展開回数は示されていないし、仮に \chop の実装に依存することを許したとしても、先の \chop の実装では、引数の文字列の長さが増えるに応じて、必要な展開回数は幾らでも増えていってしまう。これでは、「幾ら \expandafter を並べても足りない」のである。

何回展開が必要であっても完全に展開してくれるもの、といえば、\edef が思い浮かぶであろう。しかしもちろん、\edef は代入操作の一種だから、完全展開可能なマクロを実装する際には使えない。結局のところ、ここで必要なのは、“完全展開可能な \edef”である。そして、その役目を(部分的に)果たしてくれるのが \romannumeral なのである。

実際に \romannumeral を用いて問題を解決してみよう。まず、さっきの \choptwo の“ダメな実装”で、内側の \chop\romannumeral トリックを付したものを考えてみる。

\def\choptwo#1{%
  \chop{\romannumeral-`0\chop{#1}}}

なんと、これだけで、先に述べた「事前に展開回数が判らない」問題が消滅してしまう。「\romannumeral-`0\chop{filll}」はただ 1 回の展開で「fill」になることが判っているからである。あとは、引数を 1 回展開させるための単純な \expandafter 鎖を組み合わせればよいだけである。

\def\choptwo#1{%
  \expandafter\chop\expandafter{\romannumeral-`0\chop{#1}}}

これで完成である。試してみよう。

\typeout{[\choptwo{filll}][\choptwo{A}]}%==>「[fil][]」

すばらしい。

この技法が理解できたのであれば、「3 文字切り落とす」マクロ \chopthree を実装するのも極めて容易である。次のように、\chop\choptwo を“合成”すればよい。((\chop\choptwo を入れ替えたコードでもよいが、それは「\choptwo も先頭完全展開可能である」からであることに注意。))

\def\chopthree#1{%
  \expandafter\choptwo\expandafter{\romannumeral-`0\chop{#1}}}
先頭完全展開可能、が必要

ここで注意であるが、\romannumeral を用いて「関数の合成」を行う場合、対象のマクロは“先頭完全展開可能”、つまり「“先頭完全展開”するだけで“結果”のトークン列が得られる」ようなものでなければならない。

例えば、先の例の \chop を次のように実装したとする。

\def\chop#1{\xx@ifmt{#1}{}%
  {\xx@chop@a#1\xx@nil}}
\def\xx@chop@a#1#2\xx@nil{%
  \xx@ifmt{#2}{}{#1\xx@chop@a#2\xx@nil}}
% \xx@ifmt 等は前掲のコードと同じ

この \chop の実装は“完全展開可能”であるので \def\typeout の中で用いることができる。しかし、実際の \chop の展開の過程をみると次のようになっている。

\chop {filll}                (1)
→ ……(略)……
→ f\xx@chop@a illl\xx@nil   (2)
→ ……(略)……
→ fill                      (3)

\edef 中で行われる“完全展開”では、(2) のように先頭に展開不能トークンが現れてもその後に展開可能なトークンがあればその展開が行われ、(3) まで移行する。それに対して、“先頭完全展開”の場合は (2) の段階で展開が止まってしまう。従って、「\romannumeral-`0\chop{filll}」を 1 回展開した結果も「f\xx@chop@a illl\xx@nil」になってしまう。ゆえに、この \chop の実装では“合成”して \choptwo を作ることはできないのである。

(続く)

*1:以降では TeX on LaTeX を前提にする。