これは「TeX & LaTeX Advent Caleandar 2022」の25日目の記事です。
(24日目は golden_lucky さんです。)
「やっぱりTeX言語(とか)しましょう」を重点テーマとする「TeX & LaTeX Advent Calendar 2022」もいよいよクリスマスの日を迎えることになりました。最終日となる今日は「TeX言語におけるマクロの等価性」という少しニッチな話題を扱うことにします。
前提知識
というわけで、これはガチTeX言語者向けの記事です。幸せなLaTeXユーザの皆さんは例によってアドベントカレンダーのフィナーレを平和に待つことにましょう
本記事の「キホンの話」までの内容については以下の知識を仮定します。
- TeX言語の「トークン(token)」「マクロ(macro)」についてのキホン的な知識。
- 「
\ifx
が何をするものか」についての概略。
そこより先の内容については、さらに以下の知識を仮定します。
- 「文字トークンのカテゴリコード」についてのキホン的な知識。
復習の話
TeX言語における「トークンの等価性」を判定する条件文(if-トークン)には\if
・\ifcat
・\ifx
の3つがありますが、その中でも使う機会が一番多いのは\ifx
でしょう。この\ifx
は、2つのトークンの意味が等しいかを判定するif-トークンです。
-
\ifx«トークン1»«トークン2»
:〔if-トークン〕«トークン1»
と«トークン2»
の意味が同じであるか否か。
ここで、比較対象の2つのトークンの現在の意味がともにマクロ(つまり“マクロ型の値”)である場合は、「2つのマクロの等価性」が問題になります。この記事では、TeX言語においてマクロの等価性がどう決められているのかについて解説します。
準備の話
2つのトークンの等価性を判定して結果を表示するマクロを用意しておきましょう。
%% \Check<トークン1><トークン2>
% 2つのトークンの意味が等しいなら"SAME"、そうでないなら"DIFFERENT"
% を端末に表示する。
\def\Check#1#2{%
\immediate\write16{\ifx#1#2SAME\else DIFFERENT\fi}}
この\Check
マクロは以下のように使います。
\Check AA %==>SAME
\Check AB %==>DIFFERENT
以降に示す例においては、実際に\Check
を実行したときの結果を上記の形式で書くことにします。
キホンの話
まずは、フツーのTeX言語の参考書に載っているレベルの説明から始めましょう1。
2つのマクロは「次の3要素の全てについて等しい」場合に等価となります。
- 置換テキスト(replacement text)
- パラメタテキスト(parameter text)
- 接頭辞(prefix)の設定状態
これらの用語に聞きなじみのない人もいると思われるので、ちょっと補足しておきましょう。
\long\def\macroA#1;#2{#2-#1}
「置換テキスト」というのは“マクロの定義の本体”のトークン列のことで、上掲の\macroA
の定義では#2-#1
となります。また「パラメタテキスト」は“引数の書式指定”のトークン列のことで、この例では#1;#2
となります2。「接頭辞の設定状態」はこの例では「\long
付」となります。ここで考慮対象となるマクロの接頭辞には元祖TeXの範囲では\long
と\outer
があり、e-TeX拡張においてはこれに\protected
が追加されます3(\global
については後述)。
等価になる場合
最も自明なのは2つのマクロに対する定義文4が全く同じ場合です。当然“3要素”が全て同じになるので、2つのマクロは等価になります。
% パラメタテキスト=“#1;#2”
% 置換テキスト=“#2-#1”
% 接頭辞設定状態=“\long 付”
\long\def\macroA#1;#2{#2-#1}
\long\def\macroB#1;#2{#2-#1}
\Check\macroA\macroB %==>SAME
また、定義文のソースコード上での文字列が異なっていても、結局それらが同じトークン列に解釈されるのであれば、やはり等価になります。
% パラメタテキスト=“#1\and #2.”
% 置換テキスト=“#1␣\&␣#2\par”
% 接頭辞設定状態=“なし”
\def\macroA#1\and#2.{#1 \& #2\par}
% 行末は空白文字トークン、空行は \par トークンになるので
% 結局 \macroA と同じ定義文になる。
\def\macroB #1\and #2.{%
#1
\&
#2%
}
\Check\macroA\macroB %==>SAME
接頭辞設定状態は「マクロ定義時に各接頭辞が付いていたか否か」のみで区別されるので、定義文における接頭辞の出現の順序は考慮されません。
% パラメタテキスト=“#2-#1”
% 置換テキスト=“#1;#2”
% 接頭辞設定状態=“\protected・\long 付”
\long\protected\def\macroA#1;#2{#2-#1}
\protected\long\def\macroB#1;#2{#2-#1}
\Check\macroA\macroB %==>SAME
等価にならない場合
“3要素”の何れかの点で2つのマクロが異なる場合は等価になりません。
パラメタテキストが異なる例です。
%↓パラメタテキスト=“#1;#2”
\def\macroA#1;#2{#2-#1}
%↓パラメタテキスト=“#1,#2”
\def\macroB#1,#2{#2-#1}
\Check\macroA\macroB %==>DIFFERENT
置換テキストが異なる例です。
%↓置換テキスト=“#2A#1”
\def\macroA#1;#2{#2A#1}
%↓置換テキスト=“#2B#1”
\def\macroB#1;#2{#2B#1}
\Check\macroA\macroB %==>DIFFERENT
接頭辞設定状態が異なる例です。
%↓接頭辞設定状態=“\protected・\long 付”
\protected\long\def\macroA#1;#2{#2-#1}
%↓接頭辞設定状態=“\long 付”
\long\def\macroB#1;#2{#2-#1}
\Check\macroA\macroB %==>DIFFERENT
やっぱり等価にならない場合
パラメタテキストや置換テキストに対して「トークン列の一致」を判断する基準は “外形” です。以下の例では\RELAX
は\relax
と同じ意味をもちますが異なる制御綴である(つまり“外形”が異なる)ので、\macroA
と\macroB
の置換テキストは同一とは見なされません。
\let\RELAX\relax
%↓置換テキスト=“#2\relax #1”
\def\macroA#1;#2{#2\relax#1}
%↓置換テキスト=“#2\RELAX #1”
\def\macroB#1;#2{#2\RELAX#1}
%“\relax”と“\RELAX”は外形が異なるので、2つのマクロの置換テキストは異なる。
\Check\macroA\macroB %==>DIFFERENT
また「トークン列の一致」を判断する際には展開は行われません。以下の例では、\RELAX\RELAX
は完全展開すると確かに\relax\relax
になるはずですが、それでも\macroA
と\macroB
の置換テキストは異なると見なされます。
\def\RELAX{\relax}
%↓置換テキスト=“\relax \relax”
\def\macroA#1;#2{\relax\relax}
%↓置換テキスト=“\RELAX \RELAX”
\def\macroB#1;#2{\RELAX\RELAX}
% 展開せずに比較するので、2つのマクロの置換テキストは異なる。
\Check\macroA\macroB %==>DIFFERENT
なにげに等価になる場合
先ほどの説明ではマクロの接頭辞には\protected
・\long
・\outer
の3つがあると述べました。実はマクロの定義文についてはもう1つ、\global
という接頭辞も存在します。しかしこの\global
は定義の実行の様態(定義を大域的に行う)を表すものであるため、定義されるマクロ自体の状態としては反映されません。従って、定義文における\global
の有無5はマクロの等価性に影響しません。
%↓接頭辞定義状態=“なし”
\def\macroA#1;#2{#2-#1}
%↓\global は反映されないので接頭辞定義状態は“なし”となる。
\global\def\macroB#1;#2{#2-#1}
\Check\macroA\macroB %==>SAME
% \gdef は \global\def と等価なのでこれも同じ結果になる。
\gdef\macroB#1;#2{#2-#1}
\Check\macroA\macroB %==>SAME
定義文ではなくマクロ自体の中身のみが考慮されるため、マクロを\def
で定義したか\edef
で定義したかも(それ自体は)考慮されません。結果的に同じ置換テキストに帰着するのであれば、それらは等価と見なされます。
\def\RELAX{\relax}
%↓置換テキスト=“\relax \relax”
\def\macroA#1;#2{\relax\relax}
%↓置換テキストは完全展開されるので“\relax \relax”となる。
\edef\macroB#1;#2{\RELAX\RELAX}
% 結果的に両者の置換テキストは一致する。
\Check\macroA\macroB %==>SAME
次の例は先の例と似ていますが、\let
で等価定義した場合は「\relax
に展開される」わけではないので両者は異なる結果になります。
\let\RELAX\relax
%↓置換テキスト=“\relax \relax”
\def\macroA#1;#2{\relax\relax}
%↓\RELAX は展開不能なので置換テキストはそのまま“\RELAX \RELAX”となる。
\edef\macroB#1;#2{\RELAX\RELAX}
\Check\macroA\macroB %==>DIFFERENT
% もちろん後で \RELAX を展開可能に変えても \macroB 自体は何も変わらない。
\def\RELAX{\relax}
\Check\macroA\macroB %==>DIFFERENT
チョットアレな話
カテゴリコードが絡む場合
トークン列の“外形”の一致を判断する際には、文字トークンについては文字コードとカテゴリコードの両方が考慮されます。そのため、文字コードが同じでもカテゴリコードが異なる場合は等価になりません。
本記事では「カテゴリコードが11の@
の文字トークン」のことを@₁₁
のように表記します。これはTeX言語の参考書で一般的に用いられる表記法です。
%↓置換テキスト=“#2@₁₁#1”
\catcode`\@=11 \def\macroA#1;#2{#2@#1}
%↓置換テキスト=“#2@₁₂#1”、“@”のカテゴリコードが異なる。
\catcode`\@=12 \def\macroB#1;#2{#2@#1}
\Check\macroA\macroB %==>DIFFERENT
上掲の例で\macroA
と\macroB
に\meaning
を適用して得られる文字列はともに
macro:#1;#2->#2@#1
となり一致します(“\the-文字列化”する際にカテゴリコードの違いは消えてしまうため)。この例から「2つのトークンについて、その\meaning
の文字列が一致していてもそれらが等価であるとは限らない」ということがわかります。
もちろん、カテゴリコードが同じで文字コードが異なる場合も同様です。
\catcode`\<=1 \catcode`\>=2
%↓置換テキスト=“#2-₁₂\foo {₁#1}₂”
\def\macroA#1;#2{#2-\foo{#1}}
%↓置換テキスト=“#2-₁₂\foo <₁#1>₂”、文字コードが異なる。
\def\macroA#1;#2{#2-\foo<#1>}
\Check\macroA\macroB %==>DIFFERENT
このように文字トークンの“外形”には文字コードとカテゴリコードの両方の情報が含まれますが、一方で、制御綴の“外形”をみる際に「その制御綴の表記中のエスケープ文字が何であったか」は考慮されません6。従って、以下の例において\?
と!?
は“外形において等価”と見なされ、結果的に2つのマクロが等価になります。
TeX言語の参考書では制御綴のトークンを $\fbox{relax}$ のような枠付の文字列で表現することもあります。こちらの表現のほうが「制御綴の外形はエスケープ文字を含まない」ことが理解しやすいかもしれません。
\catcode`\!=0 %“!”をエスケープ文字にする
%↓置換テキスト=“#2\?\foo {#1}”
\def\macroA#1;#2\relax{#2\?\foo{#1}}
%↓“\?”と“!?”はそもそも区別されないので置換テキストは \macroA と同じになる。
\def\macroB#1;#2!relax{#2!?!foo{#1}}
\Check\macroA\macroB %==>SAME
なお、定義文におけるカテゴリコードや文字コードの違いがマクロ自体に影響しない場合もあります。例えば、以下の例では、定義文において{₁}₂
を使ったか<₁>₂
を使ったかは置換テキストには(その他の要素にも)反映されないので、2つのマクロは等価になります。
\catcode`\<=1 \catcode`\>=2
%↓全体を囲む { } 自体は置換テキストには含まれないので置換テキストは“#2-₁₂#1”。
\def\macroA#1;#2{#2-#1}
%↓こちらも < > 自体は置換テキストには含まれないので置換テキストは“#2-₁₂#1”。
\def\macroB#1;#2<#2-#1>
\Check\macroA\macroB %==>SAME
もちろん、「波括弧区切り(brace delimited)」にした場合7はその波括弧がパラメタテキストと置換テキストに含まれることになります。
\catcode`\<=1 \catcode`\>=2
%↓パラメタテキスト=“#1;₁₂#2{₁”;置換テキスト=“#2-₁₂#1{₁”
\def\macroA#1;#2#{#2-#1}
%↓パラメタテキスト=“#1;₁₂#2<₁”;置換テキスト=“#2-₁₂#1<₁”
\def\macroB#1;#2#<#2-#1>
\Check\macroA\macroB %==>DIFFERENT
パラメタトークンが絡む場合
先ほど、文字トークンの外形の一致では文字コードとカテゴリコードの両方が考慮されると述べました。ところがこれには一見例外に思えるものがあります。次のように、置換テキストにおいて文字コードが異なるパラメタ文字トークンを含めた場合、その文字コードの違いは考慮されないのです
「パラメタ文字トークン(parameter character token)」は「カテゴリコードが6である文字トークン」を指します。後で紹介する「パラメタトークン(parameter token)」と混同しないように注意してください。
\catcode`\&=6 % パラメタ文字にする
%↓置換テキストは“#₆2₁₂-₁₂#₆1₁₂”のはず…。
\def\macroA#1;#2{#2-#1}
%↓置換テキストは“#₆2₁₂-₁₂&₆1₁₂”のはず…。
\def\macroB#1;#2{#2-&1}
% なのになぜか等価であると判定される!
\Check\macroA\macroB %==>SAME
なぜパラメタ文字トークンだけが例外になるのかというと、その理由は「マクロのパラメタテキストや置換テキストの中においては、#1
のようなマクロパラメタは特殊扱いになっている」からです。具体的には、#1
のようなマクロパラメタの部分はそのまま#₆1₁₂
として保存されるのではなく、パラメタトークンと呼ばれる特殊8なトークンに置き換えられた形で保存されます。
ただしパラメタトークンは一種類だけ存在するのではありません。#1
と#2
は異なるパラメタを表すのでこの違いは保持されないといけません。このため、置換テキスト中のパラメタトークンは「#1
に相当するもの」から「#9
に相当するもの」までの9種類が存在しています。これを1ₐ
~9ₐ
で表すことにしましょう9。
そうすると、置換テキストのトークン列の保存の際には
#₆1₁₂
→1ₐ
、#₆2₁₂
→2ₐ
、……、#₆9₁₂
→9ₐ
という置き換えが発生することになります。ここで、パラメタトークンには「元のパラメタ文字トークン(#₆
など)の文字コードが何であったか」の情報は残っていないことに注意しましょう。すると、例えば&₆
のような「別の文字コードのパラメタ文字トークン」を用いたとしても
&₆1₁₂
→1ₐ
、&₆2₁₂
→2ₐ
、……、&₆9₁₂
→9ₐ
と置き換えられることになります。
これを踏まえた上で、もう一度先の例を見てみましょう。
\catcode`\&=6 % パラメタ文字にする
\def\macroA#1;#2{#2-#1}
\def\macroB#1;#2{#2-&1}
パラメタトークンでの置き換えを考慮すると以下のようになり、両者の置換テキストは一致します。
-
\macroA
の置換テキストは#₆2₁₂-₁₂#₆1₁₂
→2ₐ-₁₂1ₐ
-
\macroB
の置換テキストは#₆2₁₂-₁₂&₆1₁₂
→2ₐ-₁₂1ₐ
結局、「置換テキスト中のパラメタ文字トークンの文字コードの違い」はマクロの等価性には影響しないことがわかりました。
ところで、TeX言語のマクロ定義の文法では、パラメタ文字トークンは暗黙の文字トークン10も許容されることになっています(参考記事)。パラメタ文字トークンを暗黙にすることはマクロの等価性に影響するのでしょうか。実際に調べてみましょう。
\let\HASH=# % 暗黙のパラメタ文字トークン
%↓置換テキスト=“2ₐ-₁₂1ₐ”
\def\macroA#1;#2{#2-#1}
%↓やっぱり置換テキストは“2ₐ-₁₂1ₐ”となる。
\def\macroB#1;#2{#2-\HASH1}
\Check\macroA\macroB %==>SAME
あくまで「1番目のパラメタ」を表すパラメタトークンは1ₐ
しか存在しないので、「元のパラメタ文字トークンが暗黙か明示か」の情報も残らないのでした。
超絶アレな話
前節では「置換テキストに文字コードの異なるパラメタ文字トークンを使う」話をしました。それでは、パラメタテキストに文字コードの異なるパラメタ文字トークンを使った場合はどうなるでしょう。フツーに考えると置換テキストの場合と同じ結果になりそうですが念のため確認してみましょう……。
\catcode`\&=6 % パラメタ文字にする
%↓パラメタテキストは“1ₐ;₁₂2ₐ”のはず…。
\def\macroA#1;#2{#2-#1}
%↓パラメタテキストは“1ₐ;₁₂2ₐ”のはず…。
\def\macroB#1;&2{#2-#1}
% なのになぜか等価でないと判定される!
\Check\macroA\macroB %==>DIFFERENT
アレレ、置換テキストの場合と異なり、マクロが等価でないと判定されてしまいました。なんということでしょう
なぜこんなことになるかというと、同じ“パラメタトークン”といってもパラメタテキスト用と置換テキスト用ではその扱いが異なっているから、というのがその理由です。恐らく、内部の実装の都合でそうしているのがマクロの等価性という性質を通して“微妙に外に漏れてしまっている”というアレな事案なのでしょう
パラメタテキスト用のパラメタトークンが置換テキスト用のそれと異なる点は「文字コードを保持している」ことです。つまり(符号空間が8ビットである元祖TeXでいうと)「文字コードが0のもの」から「文字コードが255のもの」までの256種類が存在するわけです。そこで、例えばX
の文字コードをもつ(パラメタテキスト用の)パラメタトークンをXₚ
で表すことにしましょう11。パラメタテキストのトークン列中でのマクロパラメタは以下のように置き換えられます。
X₆1₁₂
→Xₚ
、X₆2₁₂
→Xₚ
、……、X₆9₁₂
→Xₚ
(X
は任意の文字)
おや、パラメタの番号(1~9)の情報が消えてしまっています。でもこれで問題はありません。なぜかというと「パラメタテキストにおいては12マクロパラメタは番号順に並んでいなければならない」という規則が存在するからです13。
先の例をもう一度見てみましょう。
\catcode`\&=6 % パラメタ文字にする
\def\macroA#1;#2{#2-#1}
\def\macroB#1;&2{#2-#1}
先述の規則を適用すると、パラメタトークンでの置き換えの結果は以下のようになります。
-
\macroA
のパラメタテキストは#₆1₁₂;₁₂#₆2₁₂
→#ₚ;₁₂#ₚ
-
\macroB
のパラメタテキストは#₆1₁₂;₁₂&₆2₁₂
→#ₚ;₁₂&ₚ
この通り、#ₚ
と&ₚ
は異なるパラメタトークンであるため、2つのマクロのパラメタテキストは一致しないのでした。
前節で「暗黙のパラメタ文字トークンを置換テキストに使う」話をしましたが、容易に予想されるように、暗黙のパラメタ文字トークンはパラメタテキストでも使用できます。
\let\HASH=# % 暗黙のパラメタ文字トークン
\def\macroA#1;#2{#2-#1}
\def\macroB#1;\HASH2{#2-#1}
この場合に\macroA
と\macroB
は等価になるでしょうか。これまでの解説を読んでいれば答えがわかるはずなので、一度自分でじっくりと考えてみてください
正解は……。
\let\HASH=# % 暗黙のパラメタ文字トークン
%↓パラメタテキスト=“#ₚ;₁₂#ₚ”
\def\macroA#1;#2{#2-#1}
%↓パラメタテキスト=“#ₚ;₁₂#ₚ”
\def\macroB#1;\HASH2{#2-#1}
% 両者のパラメタテキストが一致するので等価になる。
\Check\macroA\macroB %==>SAME
この通り等価になります。パラメタテキスト用のパラメタトークンは文字コードの情報はもっていますが「元のパラメタ文字トークンが暗黙であったか」の情報をもつ余地はありません。明示と暗黙の#₆
はともに#ₚ
に置き換えられて区別がつかないのでした。
おつかれさまでした
まとめ
「メリークリスマス!」
-
「マクロの等価性」は専ら
\ifx
での判定で使われる概念なので、TeX言語の参考書においては\ifx
の解説のところで扱われることが多いはずです。 ↩ -
「パラメタトークン」の扱いについては後で詳しく述べます。それまでは取りあえず「
#1
はそのまま2トークンとして扱われる」と思ってかまいません。 ↩ -
これらの接頭辞の機能が何かを説明することは本記事のスコープ外とします。 ↩
-
本記事では「マクロを定義する作用をもつ(
\def
・\edef
・\gdef
・\xdef
のプリミティブをもつ)文」のことを定義文と呼ぶことにします。 ↩ -
\def
と\gdef
の違い(および\edef
と\xdef
の違い)についても同様です。 ↩ -
そもそも制御綴のトークンは
\csname
でも生成できるわけで、この場合には「制御綴の表記中のエスケープ文字」という概念は意味を成しません。 ↩ -
マクロ定義文のパラメタテキストの末尾、つまり
{₁
(等のグループ開始文字トークン)の直前に#₆
(等のパラメタ文字トークン)を置くと、そのマクロは「波括弧区切り」になります。この定義は「パラメタテキストと置換テキストの両方の末尾に当該のグループ開始文字トークンを追加する」という処理によって実現されます。 ↩ -
パラメタトークンはマクロ(“マクロ型の値”)を構成するパラメタテキストや置換テキストのトークンの中にのみ存在するという意味で、極めて特殊な存在です。入力バッファのトークンの中には決して含まれないため、例えば「パラメタトークンと等価な制御綴」のようなものを作り出すことは原理的にできません。 ↩
-
添字の
a
は“argument”(引数)の略です。 ↩ -
制御綴やアクティブ文字に対して非アクティブ文字を
\let
したもの。 ↩ -
添字の
p
は“parameter”(パラメタ)の略です。 ↩ -
先の例で
\macroA
や\macroB
の置換テキストが#2-#1
であることからわかるように、「マクロパラメタは番号順に並ぶ」という性質は置換テキストにおいては成立しません。 ↩ -
例えばパラメタトークンで置き換えた後のパラメタテキストが
_₈Aₚ_₈Bₚ_₈Cₚ_₈
だったとすると、Aₚ
・Bₚ
・Cₚ
がそれぞれ1番目・2番目・3番目のマクロパラメタであることは明らかです。つまり元のトークン列は_A₆1_B₆2_C₆3_
だったわけです。 ↩